From 387cfb0aa23b20d30aa28b9dbdc6e2462acbcfc4 Mon Sep 17 00:00:00 2001 From: talatops Date: Wed, 4 Mar 2026 21:52:42 +0500 Subject: [PATCH] Refine role-aware login and theming Made-with: Cursor --- README.md | 16 +++++- src/app/dashboard/layout.tsx | 4 +- src/app/login/page.tsx | 101 +++++++++++++++++++++++++++++++++-- src/app/page.tsx | 22 ++++++-- src/app/supplier/layout.tsx | 5 +- src/auth.ts | 44 ++++++++++++++- 6 files changed, 177 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1322982..1d31a78 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ ## JUNO -![JUNO logo](./public/icon.png) +

+ JUNO logo +

-**Tech stack:** ⚛️ Next.js · 💙 TypeScript · 🐘 PostgreSQL · ☁️ AWS Bedrock · 📬 Gmail API · 🐳 Docker · 🔐 NextAuth +**Tech stack** + +![Next.js](https://img.shields.io/badge/Next.js-000000?logo=nextdotjs&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) +![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?logo=postgresql&logoColor=white) +![AWS Bedrock](https://img.shields.io/badge/AWS%20Bedrock-232F3E?logo=amazonaws&logoColor=white) +![Gmail API](https://img.shields.io/badge/Gmail%20API-EA4335?logo=gmail&logoColor=white) +![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white) +![NextAuth](https://img.shields.io/badge/NextAuth-000000?logo=nextdotjs&logoColor=white) +![Shopify API](https://img.shields.io/badge/Shopify%20API-7AB55C?logo=shopify&logoColor=white) +![AES Encryption](https://img.shields.io/badge/AES%20Encryption-4B0082) Modern commerce and support infrastructure for Shopify‑based dropshipping businesses. 🚀 diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index eac7022..a586d0b 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -21,9 +21,9 @@ export default async function DashboardLayout({ if (session.user.role !== "VENDOR") { if (session.user.role === "SUPPLIER") { - redirect("/supplier") + redirect("/login?error=WrongPortal&role=SUPPLIER") } - redirect("/login") + redirect("/login?error=WrongRole") } const { prisma } = await import("@/lib/prisma") diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 41f1363..e2b94f1 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,21 +1,62 @@ "use client" import { signIn } from "next-auth/react" -import { useState } from "react" -import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" import { motion } from "framer-motion" -import { Mail, Lock, Loader2, Shield } from "lucide-react" +import { Mail, Lock, Loader2, Shield, ShoppingBag, Truck } from "lucide-react" import Link from "next/link" import Image from "next/image" export default function LoginPage() { const router = useRouter() + const searchParams = useSearchParams() + const initialRoleFromQuery = + searchParams.get("role")?.toUpperCase() === "SUPPLIER" ? "SUPPLIER" : "VENDOR" + const [loading, setLoading] = useState(false) const [error, setError] = useState("") const [requiresMFA, setRequiresMFA] = useState(false) const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [mfaCode, setMfaCode] = useState("") + const [role, setRole] = useState<"VENDOR" | "SUPPLIER">(initialRoleFromQuery) + + const applyRoleCookie = (selectedRole: "VENDOR" | "SUPPLIER") => { + try { + document.cookie = `juno_login_role=${selectedRole}; path=/; max-age=600` + } catch { + // ignore cookie errors – server-side will just skip role enforcement + } + } + + // Map Auth.js error codes from query string to friendly messages + useEffect(() => { + const errorCode = searchParams.get("error") + const existingRole = searchParams.get("existingRole") + + if (errorCode === "SigninRoleMismatch") { + if (existingRole === "SUPPLIER") { + setError( + "This email is registered as a Supplier. Please choose the Supplier option or use another email." + ) + setRole("SUPPLIER") + } else if (existingRole === "VENDOR") { + setError( + "This email is registered as a Vendor. Please choose the Vendor option or use another email." + ) + setRole("VENDOR") + } else { + setError( + "This email is already registered with a different role. Please select the correct role or use another email." + ) + } + } else if (errorCode === "WrongRole" || errorCode === "WrongPortal") { + setError( + "You tried to open a different portal with this account. Please sign in with the correct role or use a separate account." + ) + } + }, [searchParams]) const handleCredentialsLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -69,6 +110,9 @@ export default function LoginPage() { return } + // Ensure the selected role is visible to the auth callbacks + applyRoleCookie(role) + // If MFA was required and code provided, or MFA not required, proceed with NextAuth signIn const res = await signIn("credentials", { email: emailValue, @@ -94,6 +138,7 @@ export default function LoginPage() { } const handleGoogleSignIn = () => { + applyRoleCookie(role) signIn("google", { callbackUrl: "/onboarding" }) } @@ -127,6 +172,56 @@ export default function LoginPage() {

+
+ + + +
+
diff --git a/src/app/page.tsx b/src/app/page.tsx index bda4d65..20f354b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,19 +7,31 @@ import { LandingFeatures } from "@/components/landing/LandingFeatures" import { LandingFooter } from "@/components/landing/LandingFooter" import { MobileHero } from "@/components/landing/mobile/MobileHero" import { MobileFeatures } from "@/components/landing/mobile/MobileFeatures" +import { useTheme } from "@/components/ThemeProvider" +import { cn } from "@/lib/utils" import dynamic from "next/dynamic" -const LandingSolutions = dynamic(() => import("@/components/landing/LandingSolutions").then(mod => mod.LandingSolutions), { - ssr: true, - loading: () =>
-}) +const LandingSolutions = dynamic( + () => import("@/components/landing/LandingSolutions").then((mod) => mod.LandingSolutions), + { + ssr: true, + loading: () =>
, + } +) export default function HomePage() { const isMobile = useMediaQuery("(max-width: 768px)") + const { theme } = useTheme() + const isLight = theme === "light" return ( -
+
{isMobile ? ( diff --git a/src/app/supplier/layout.tsx b/src/app/supplier/layout.tsx index b6b21e1..865ea36 100644 --- a/src/app/supplier/layout.tsx +++ b/src/app/supplier/layout.tsx @@ -20,7 +20,10 @@ export default async function SupplierLayout({ // Only allow SUPPLIER role to access supplier dashboard if (session.user.role !== "SUPPLIER") { - redirect("/dashboard") + if (session.user.role === "VENDOR") { + redirect("/login?error=WrongPortal&role=VENDOR") + } + redirect("/login?error=WrongRole") } // Ensure supplier profile exists, create if not diff --git a/src/auth.ts b/src/auth.ts index 422e607..338b269 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,6 +3,7 @@ import { PrismaAdapter } from "@auth/prisma-adapter" import { prisma } from "@/lib/prisma" import authConfig from "./auth.config" import bcrypt from "bcryptjs" +import { cookies } from "next/headers" const ADMIN_EMAIL = process.env.ADMIN_EMAIL const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD @@ -13,6 +14,45 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ trustHost: true, // Trust all hosts (needed for ngrok/proxies) ...authConfig, callbacks: { + async signIn({ user, account }) { + // Allow non-email-based sign-in flows to proceed as usual + if (!user?.email) return true + + // Never constrain the admin account via role toggle + if (user.email === ADMIN_EMAIL || user.role === "ADMIN") { + return true + } + + // Read the desired role from the role toggle cookie set on the client + const cookieStore = await cookies() + const cookieRole = cookieStore.get("juno_login_role")?.value + const desiredRole = + cookieRole === "SUPPLIER" || cookieRole === "VENDOR" ? cookieRole : null + + // If no role was explicitly chosen (e.g. legacy flows), fall back to existing behaviour + if (!desiredRole) { + return true + } + + const existingUser = await prisma.user.findUnique({ + where: { email: user.email }, + select: { id: true, role: true }, + }) + + // New user via OAuth: align their role with the chosen role on first sign-in + if (existingUser && existingUser.role !== desiredRole) { + // If the role already exists and differs, block sign-in with a clear error code + const url = new URL("/login", process.env.NEXTAUTH_URL || "http://localhost:3000") + url.searchParams.set("error", "SigninRoleMismatch") + url.searchParams.set("existingRole", existingUser.role || "VENDOR") + url.searchParams.set("desiredRole", desiredRole) + return url.toString() + } + + // If the user record exists but has the default role and this is effectively a first login + // we keep the current behaviour and let onboarding/layouts route correctly. + return true + }, async jwt({ token, user }) { if (user) { token.id = user.id @@ -89,6 +129,6 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ return user }, - } as unknown as import("@auth/core/providers").Provider - ] + } as unknown as import("@auth/core/providers").Provider, + ], })