fix auth flow
This commit is contained in:
		
							parent
							
								
									b8c67992cb
								
							
						
					
					
						commit
						e689384977
					
				
							
								
								
									
										11
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								.env.example
									
									
									
									
									
								
							| @ -1,11 +1,6 @@ | |||||||
| NEXT_PUBLIC_API_BASE_URL=http://localhost:3005 | NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" | ||||||
|  | INTERNAL_API_BASE_URL="http://localhost:3005" | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| DATABASE_URL="postgresql://postgres:root@localhost:5432/rooftop?schema=public" | DATABASE_URL="postgresql://postgres:root@localhost:5432/rooftop?schema=public" | ||||||
| JWT_SECRET="secret_key" | JWT_SECRET="secret_key" | ||||||
| 
 |  | ||||||
| #SUNGROW |  | ||||||
| SUNGROW_SECRET_KEY= |  | ||||||
| SUNGROW_APP_KEY= |  | ||||||
| 
 |  | ||||||
| #CHINT |  | ||||||
| NEXT_PUBLIC_CHINT_TOKEN= |  | ||||||
|  | |||||||
| @ -27,8 +27,8 @@ jobs: | |||||||
|       - name: Build |       - name: Build | ||||||
|         run: npm run build |         run: npm run build | ||||||
|         env: |         env: | ||||||
|           NEXT_PUBLIC_URL: 'http://localhost:3000' |           NEXT_PUBLIC_URL: 'http://localhost:3005' | ||||||
|           NEXT_PUBLIC_FORECAST_URL: 'http://localhost:3001' |           NEXT_PUBLIC_FORECAST_URL: 'http://localhost:3005' | ||||||
|           DATABASE_URL: 'postgresql://dummy:dummy@localhost:5432/dummy' |           DATABASE_URL: 'postgresql://dummy:dummy@localhost:5432/dummy' | ||||||
|           SMTP_EMAIL: 'dummy@example.com' |           SMTP_EMAIL: 'dummy@example.com' | ||||||
|           SMTP_EMAIL_PASSWORD: 'dummy' |           SMTP_EMAIL_PASSWORD: 'dummy' | ||||||
|  | |||||||
| @ -59,6 +59,7 @@ const AdminDashboard = () => { | |||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|   const pathname = usePathname(); |   const pathname = usePathname(); | ||||||
|   const searchParams = useSearchParams(); |   const searchParams = useSearchParams(); | ||||||
|  |   const [authChecked, setAuthChecked] = useState(false); | ||||||
| 
 | 
 | ||||||
|   // --- load CRM projects dynamically ---
 |   // --- load CRM projects dynamically ---
 | ||||||
|   const [sites, setSites] = useState<CrmProject[]>([]); |   const [sites, setSites] = useState<CrmProject[]>([]); | ||||||
| @ -67,6 +68,30 @@ const AdminDashboard = () => { | |||||||
|   // near other refs
 |   // near other refs
 | ||||||
|   const loggingRef = useRef<HTMLDivElement | null>(null); |   const loggingRef = useRef<HTMLDivElement | null>(null); | ||||||
| 
 | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const checkAuth = async () => { | ||||||
|  |       try { | ||||||
|  |         const res = await fetch('/api/auth/me', { credentials: 'include' }); | ||||||
|  |         if (!res.ok) { | ||||||
|  |           router.replace('/login'); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         const data = await res.json(); | ||||||
|  |         if (!data.user) { | ||||||
|  |           router.replace('/login'); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       } catch { | ||||||
|  |         router.replace('/login'); | ||||||
|  |       } finally { | ||||||
|  |         setAuthChecked(true); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     checkAuth(); | ||||||
|  |   }, [router]); | ||||||
|  | 
 | ||||||
|  |    | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     setSitesLoading(true); |     setSitesLoading(true); | ||||||
| @ -288,7 +313,7 @@ const AdminDashboard = () => { | |||||||
|             setHasTodayData(true);   // and today has data too
 |             setHasTodayData(true);   // and today has data too
 | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|         } catch { |   } catch { | ||||||
|           // ignore and keep polling
 |           // ignore and keep polling
 | ||||||
|         } |         } | ||||||
|         await new Promise(r => setTimeout(r, 3000)); |         await new Promise(r => setTimeout(r, 3000)); | ||||||
| @ -300,6 +325,10 @@ const AdminDashboard = () => { | |||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   // ---------- RENDER ----------
 |   // ---------- RENDER ----------
 | ||||||
|  |   if (!authChecked) { | ||||||
|  |     return <div>Checking authentication…</div>; | ||||||
|  |   } | ||||||
|  |    | ||||||
|   if (sitesLoading) { |   if (sitesLoading) { | ||||||
|     return ( |     return ( | ||||||
|       <DashboardLayout> |       <DashboardLayout> | ||||||
|  | |||||||
| @ -1,73 +1,100 @@ | |||||||
|  | // app/login/page.tsx
 | ||||||
|  | 'use client'; | ||||||
|  | 
 | ||||||
| import Link from 'next/link'; | import Link from 'next/link'; | ||||||
| import { Metadata } from 'next'; | import React, { useEffect, useState } from 'react'; | ||||||
| import React from 'react'; | import { useRouter } from 'next/navigation'; | ||||||
| import ComponentsAuthLoginForm from '@/components/auth/components-auth-login-form'; | import ComponentsAuthLoginForm from '@/components/auth/components-auth-login-form'; | ||||||
| 
 | 
 | ||||||
| type Props = {} | export default function LoginPage() { | ||||||
|  |   const router = useRouter(); | ||||||
|  |   const [ready, setReady] = useState(false); // gate to avoid UI flash
 | ||||||
| 
 | 
 | ||||||
| const LoginPage = (props: Props) => { |   useEffect(() => { | ||||||
|     return ( |     let cancelled = false; | ||||||
|         <div className="relative min-h-screen overflow-hidden bg-[#060818] text-white"> |  | ||||||
|             {/* Background gradient layer */} |  | ||||||
|             <div className="absolute inset-0 -z-10"> |  | ||||||
|                 <img |  | ||||||
|                     src="/assets/images/auth/bg-gradient.png" |  | ||||||
|                     alt="background gradient" |  | ||||||
|                     className="h-full w-full object-cover" |  | ||||||
|                 /> |  | ||||||
|                 <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" /> |  | ||||||
|             </div> |  | ||||||
| 
 | 
 | ||||||
|             {/* Background decorative objects */} |     (async () => { | ||||||
|             <img |       try { | ||||||
|                 src="/assets/images/auth/coming-soon-object1.png" |         const res = await fetch('/api/auth/me', { | ||||||
|                 alt="left decor" |           method: 'GET', | ||||||
|                 className="absolute left-0 top-1/2 hidden h-full max-h-[893px] -translate-y-1/2 brightness-125 md:block" |           cache: 'no-store', | ||||||
|             /> |           credentials: 'include', // safe even if same-origin
 | ||||||
|             <img |         }); | ||||||
|                 src="/assets/images/auth/coming-soon-object3.png" |         if (!cancelled && res.ok) { | ||||||
|                 alt="right decor" |           router.replace('/adminDashboard'); | ||||||
|                 className="absolute right-0 top-0 hidden h-[300px] brightness-125 md:block" |           return; | ||||||
|             /> |         } | ||||||
|  |       } catch { | ||||||
|  |         // ignore errors; just show the form
 | ||||||
|  |       } | ||||||
|  |       if (!cancelled) setReady(true); | ||||||
|  |     })(); | ||||||
| 
 | 
 | ||||||
|             {/* Centered card wrapper */} |     return () => { cancelled = true; }; | ||||||
|             <div className="relative flex min-h-screen items-center justify-center px-6 py-10 sm:px-16"> |   }, [router]); | ||||||
|                 <div | 
 | ||||||
|                     className="relative w-full max-w-[870px] rounded-2xl p-1 |   if (!ready) return null; // or a small spinner if you prefer
 | ||||||
|                                bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)] | 
 | ||||||
|                                dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]" |   return ( | ||||||
|  |     <div className="relative min-h-screen overflow-hidden bg-[#060818] text-white"> | ||||||
|  |       {/* Background gradient layer */} | ||||||
|  |       <div className="absolute inset-0 -z-10"> | ||||||
|  |         <img | ||||||
|  |           src="/assets/images/auth/bg-gradient.png" | ||||||
|  |           alt="background gradient" | ||||||
|  |           className="h-full w-full object-cover" | ||||||
|  |         /> | ||||||
|  |         <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" /> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       {/* Background decorative objects */} | ||||||
|  |       <img | ||||||
|  |         src="/assets/images/auth/coming-soon-object1.png" | ||||||
|  |         alt="left decor" | ||||||
|  |         className="absolute left-0 top-1/2 hidden h-full max-h-[893px] -translate-y-1/2 brightness-125 md:block" | ||||||
|  |       /> | ||||||
|  |       <img | ||||||
|  |         src="/assets/images/auth/coming-soon-object3.png" | ||||||
|  |         alt="right decor" | ||||||
|  |         className="absolute right-0 top-0 hidden h-[300px] brightness-125 md:block" | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       {/* Centered card wrapper */} | ||||||
|  |       <div className="relative flex min-h-screen items-center justify-center px-6 py-10 sm:px-16"> | ||||||
|  |         <div | ||||||
|  |           className="relative w-full max-w-[870px] rounded-2xl p-1 | ||||||
|  |                      bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)] | ||||||
|  |                      dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]" | ||||||
|  |         > | ||||||
|  |           {/* Inner card (glassmorphic effect) */} | ||||||
|  |           <div className="relative z-10 rounded-2xl bg-white/10 px-8 py-16 backdrop-blur-lg dark:bg-white/10 lg:min-h-[600px]"> | ||||||
|  |             <div className="mx-auto w-full max-w-[440px] text-center"> | ||||||
|  |               {/* Header */} | ||||||
|  |               <h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2"> | ||||||
|  |                 Sign In | ||||||
|  |               </h1> | ||||||
|  |               <p className="text-base font-medium text-gray-200 dark:text-gray-300 mb-8"> | ||||||
|  |                 Enter your email and password to access your account. | ||||||
|  |               </p> | ||||||
|  | 
 | ||||||
|  |               {/* Login form */} | ||||||
|  |               <ComponentsAuthLoginForm /> | ||||||
|  | 
 | ||||||
|  |               {/* Footer link */} | ||||||
|  |               <div className="mt-6 text-sm text-gray-200 dark:text-gray-300"> | ||||||
|  |                 Don’t have an account?{' '} | ||||||
|  |                 <Link | ||||||
|  |                   href="/register" | ||||||
|  |                   className="text-yellow-400 font-semibold underline transition hover:text-white" | ||||||
|                 > |                 > | ||||||
|                     {/* Inner card (glassmorphic effect) */} |                   SIGN UP | ||||||
|                     <div className="relative z-10 rounded-2xl bg-white/10 px-8 py-16 backdrop-blur-lg dark:bg-white/10 lg:min-h-[600px]"> |                 </Link> | ||||||
|                         <div className="mx-auto w-full max-w-[440px] text-center"> |               </div> | ||||||
|                             {/* Header */} |  | ||||||
|                             <h1 className="text-4xl font-extrabold uppercase tracking-wide text-yellow-400 mb-2"> |  | ||||||
|                                 Sign In |  | ||||||
|                             </h1> |  | ||||||
|                             <p className="text-base font-medium text-gray-200 dark:text-gray-300 mb-8"> |  | ||||||
|                                 Enter your email and password to access your account. |  | ||||||
|                             </p> |  | ||||||
| 
 |  | ||||||
|                             {/* Login form */} |  | ||||||
|                             <ComponentsAuthLoginForm /> |  | ||||||
| 
 |  | ||||||
|                             {/* Footer link */} |  | ||||||
|                             <div className="mt-6 text-sm text-gray-200 dark:text-gray-300"> |  | ||||||
|                                 Don’t have an account?{" "} |  | ||||||
|                                 <Link |  | ||||||
|                                     href="/register" |  | ||||||
|                                     className="text-yellow-400 font-semibold underline transition hover:text-white" |  | ||||||
|                                 > |  | ||||||
|                                     SIGN UP |  | ||||||
|                                 </Link> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|             </div> |             </div> | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|     ); |       </div> | ||||||
| }; |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| export default LoginPage |  | ||||||
|  | |||||||
| @ -72,7 +72,7 @@ useEffect(() => { | |||||||
| }, [pathname]); // re-fetch on route change (after login redirect)
 | }, [pathname]); // re-fetch on route change (after login redirect)
 | ||||||
| 
 | 
 | ||||||
|   const handleLogout = async () => { |   const handleLogout = async () => { | ||||||
|     await fetch('/api/auth/logout', { method: 'POST' }); |     await fetch('/api/logout', { method: 'POST' }); | ||||||
|     setUser(null); |     setUser(null); | ||||||
|     router.push('/login'); // go to login
 |     router.push('/login'); // go to login
 | ||||||
|   }; |   }; | ||||||
|  | |||||||
| @ -10,10 +10,10 @@ export function middleware(req: NextRequest) { | |||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         jwt.verify(token, SECRET_KEY); |         jwt.verify(token, SECRET_KEY); | ||||||
|         return NextResponse.next(); |     return NextResponse.next(); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         return NextResponse.redirect(new URL("/login", req.url)); |         return NextResponse.redirect(new URL("/login", req.url)); | ||||||
|     } |     } | ||||||
| } |   } | ||||||
| 
 | 
 | ||||||
| export const config = { matcher: ["/dashboard", "/profile"] }; | export const config = { matcher: ["/dashboard", "/profile"] }; | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -31,6 +31,7 @@ | |||||||
|                 "he": "^1.2.0", |                 "he": "^1.2.0", | ||||||
|                 "html2canvas": "^1.4.1", |                 "html2canvas": "^1.4.1", | ||||||
|                 "i18next": "^22.4.10", |                 "i18next": "^22.4.10", | ||||||
|  |                 "jose": "^6.0.12", | ||||||
|                 "jsonwebtoken": "^9.0.2", |                 "jsonwebtoken": "^9.0.2", | ||||||
|                 "jspdf": "^3.0.1", |                 "jspdf": "^3.0.1", | ||||||
|                 "next": "14.0.3", |                 "next": "14.0.3", | ||||||
| @ -7107,6 +7108,15 @@ | |||||||
|                 "jiti": "bin/jiti.js" |                 "jiti": "bin/jiti.js" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/jose": { | ||||||
|  |             "version": "6.0.12", | ||||||
|  |             "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", | ||||||
|  |             "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "funding": { | ||||||
|  |                 "url": "https://github.com/sponsors/panva" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/js-sdsl": { |         "node_modules/js-sdsl": { | ||||||
|             "version": "4.2.0", |             "version": "4.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", |             "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", | ||||||
| @ -14693,6 +14703,11 @@ | |||||||
|             "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", |             "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", | ||||||
|             "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==" |             "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==" | ||||||
|         }, |         }, | ||||||
|  |         "jose": { | ||||||
|  |             "version": "6.0.12", | ||||||
|  |             "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.12.tgz", | ||||||
|  |             "integrity": "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==" | ||||||
|  |         }, | ||||||
|         "js-sdsl": { |         "js-sdsl": { | ||||||
|             "version": "4.2.0", |             "version": "4.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", |             "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ | |||||||
|         "he": "^1.2.0", |         "he": "^1.2.0", | ||||||
|         "html2canvas": "^1.4.1", |         "html2canvas": "^1.4.1", | ||||||
|         "i18next": "^22.4.10", |         "i18next": "^22.4.10", | ||||||
|  |         "jose": "^6.0.12", | ||||||
|         "jsonwebtoken": "^9.0.2", |         "jsonwebtoken": "^9.0.2", | ||||||
|         "jspdf": "^3.0.1", |         "jspdf": "^3.0.1", | ||||||
|         "next": "14.0.3", |         "next": "14.0.3", | ||||||
|  | |||||||
| @ -1,20 +1,22 @@ | |||||||
| // pages/api/auth/logout.ts
 | // pages/api/logout.ts  ->  /api/logout
 | ||||||
| import type { NextApiRequest, NextApiResponse } from "next"; | import type { NextApiRequest, NextApiResponse } from 'next'; | ||||||
|  | import { serialize } from 'cookie'; | ||||||
| 
 | 
 | ||||||
| export default function handler(req: NextApiRequest, res: NextApiResponse) { | export default function handler(req: NextApiRequest, res: NextApiResponse) { | ||||||
|   const isProd = process.env.NODE_ENV === "production"; |   const isProd = process.env.NODE_ENV === 'production'; | ||||||
|   res.setHeader( | 
 | ||||||
|     "Set-Cookie", |   const setCookie = serialize('token', '', { | ||||||
|     [ |     httpOnly: true, | ||||||
|       "token=", // empty token
 |     secure: isProd, | ||||||
|       "HttpOnly", |     sameSite: 'strict',  // matches login
 | ||||||
|       "Path=/", |     path: '/',           // matches login
 | ||||||
|       "SameSite=Strict", |     maxAge: 0, | ||||||
|       "Max-Age=0", // expire immediately
 |     expires: new Date(0), | ||||||
|       isProd ? "Secure" : "", |   }); | ||||||
|     ] | 
 | ||||||
|       .filter(Boolean) |   res.setHeader('Set-Cookie', setCookie); | ||||||
|       .join("; ") |   res.setHeader('Cache-Control', 'no-store'); | ||||||
|   ); |   return res.status(200).json({ message: 'Logged out' }); | ||||||
|   return res.status(200).json({ message: "Logged out" }); |  | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user