From e689384977274f5f5f94e578ad553c74f57daecc Mon Sep 17 00:00:00 2001 From: Syasya Date: Wed, 20 Aug 2025 12:41:52 +0800 Subject: [PATCH] fix auth flow --- .env.example | 11 +- .gitea/workflows/pr-build-check.yml | 4 +- app/(admin)/adminDashboard/page.tsx | 31 +++++- app/(auth)/login/page.tsx | 153 ++++++++++++++++------------ components/layouts/header.tsx | 2 +- middleware.ts | 4 +- package-lock.json | 15 +++ package.json | 1 + pages/api/logout.ts | 36 +++---- 9 files changed, 163 insertions(+), 94 deletions(-) diff --git a/.env.example b/.env.example index 0150039..48af7c8 100644 --- a/.env.example +++ b/.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" JWT_SECRET="secret_key" - -#SUNGROW -SUNGROW_SECRET_KEY= -SUNGROW_APP_KEY= - -#CHINT -NEXT_PUBLIC_CHINT_TOKEN= diff --git a/.gitea/workflows/pr-build-check.yml b/.gitea/workflows/pr-build-check.yml index 7673ebe..5022fc2 100644 --- a/.gitea/workflows/pr-build-check.yml +++ b/.gitea/workflows/pr-build-check.yml @@ -27,8 +27,8 @@ jobs: - name: Build run: npm run build env: - NEXT_PUBLIC_URL: 'http://localhost:3000' - NEXT_PUBLIC_FORECAST_URL: 'http://localhost:3001' + NEXT_PUBLIC_URL: 'http://localhost:3005' + NEXT_PUBLIC_FORECAST_URL: 'http://localhost:3005' DATABASE_URL: 'postgresql://dummy:dummy@localhost:5432/dummy' SMTP_EMAIL: 'dummy@example.com' SMTP_EMAIL_PASSWORD: 'dummy' diff --git a/app/(admin)/adminDashboard/page.tsx b/app/(admin)/adminDashboard/page.tsx index bc97fce..3fdf0cf 100644 --- a/app/(admin)/adminDashboard/page.tsx +++ b/app/(admin)/adminDashboard/page.tsx @@ -59,6 +59,7 @@ const AdminDashboard = () => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); + const [authChecked, setAuthChecked] = useState(false); // --- load CRM projects dynamically --- const [sites, setSites] = useState([]); @@ -67,6 +68,30 @@ const AdminDashboard = () => { // near other refs const loggingRef = useRef(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(() => { setSitesLoading(true); @@ -288,7 +313,7 @@ const AdminDashboard = () => { setHasTodayData(true); // and today has data too break; } - } catch { + } catch { // ignore and keep polling } await new Promise(r => setTimeout(r, 3000)); @@ -300,6 +325,10 @@ const AdminDashboard = () => { }; // ---------- RENDER ---------- + if (!authChecked) { + return
Checking authentication…
; + } + if (sitesLoading) { return ( diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index ef76b0c..ee9e9bc 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,73 +1,100 @@ +// app/login/page.tsx +'use client'; + import Link from 'next/link'; -import { Metadata } from 'next'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; 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) => { - return ( -
- {/* Background gradient layer */} -
- background gradient -
-
+ useEffect(() => { + let cancelled = false; - {/* Background decorative objects */} - left decor - right decor + (async () => { + try { + const res = await fetch('/api/auth/me', { + method: 'GET', + cache: 'no-store', + credentials: 'include', // safe even if same-origin + }); + if (!cancelled && res.ok) { + router.replace('/adminDashboard'); + return; + } + } catch { + // ignore errors; just show the form + } + if (!cancelled) setReady(true); + })(); - {/* Centered card wrapper */} -
-
{ cancelled = true; }; + }, [router]); + + if (!ready) return null; // or a small spinner if you prefer + + return ( +
+ {/* Background gradient layer */} +
+ background gradient +
+
+ + {/* Background decorative objects */} + left decor + right decor + + {/* Centered card wrapper */} +
+
+ {/* Inner card (glassmorphic effect) */} +
+
+ {/* Header */} +

+ Sign In +

+

+ Enter your email and password to access your account. +

+ + {/* Login form */} + + + {/* Footer link */} +
+ Don’t have an account?{' '} + - {/* Inner card (glassmorphic effect) */} -
-
- {/* Header */} -

- Sign In -

-

- Enter your email and password to access your account. -

- - {/* Login form */} - - - {/* Footer link */} -
- Don’t have an account?{" "} - - SIGN UP - -
-
-
-
+ SIGN UP + +
+
- ); -}; +
+
+ ); +} - -export default LoginPage diff --git a/components/layouts/header.tsx b/components/layouts/header.tsx index 19402f0..e03b460 100644 --- a/components/layouts/header.tsx +++ b/components/layouts/header.tsx @@ -72,7 +72,7 @@ useEffect(() => { }, [pathname]); // re-fetch on route change (after login redirect) const handleLogout = async () => { - await fetch('/api/auth/logout', { method: 'POST' }); + await fetch('/api/logout', { method: 'POST' }); setUser(null); router.push('/login'); // go to login }; diff --git a/middleware.ts b/middleware.ts index c499d26..e9b2947 100644 --- a/middleware.ts +++ b/middleware.ts @@ -10,10 +10,10 @@ export function middleware(req: NextRequest) { try { jwt.verify(token, SECRET_KEY); - return NextResponse.next(); + return NextResponse.next(); } catch (error) { return NextResponse.redirect(new URL("/login", req.url)); } -} + } export const config = { matcher: ["/dashboard", "/profile"] }; diff --git a/package-lock.json b/package-lock.json index dba53c1..98ba282 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "he": "^1.2.0", "html2canvas": "^1.4.1", "i18next": "^22.4.10", + "jose": "^6.0.12", "jsonwebtoken": "^9.0.2", "jspdf": "^3.0.1", "next": "14.0.3", @@ -7107,6 +7108,15 @@ "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": { "version": "4.2.0", "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", "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": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", diff --git a/package.json b/package.json index 83a132d..f1be8f8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "he": "^1.2.0", "html2canvas": "^1.4.1", "i18next": "^22.4.10", + "jose": "^6.0.12", "jsonwebtoken": "^9.0.2", "jspdf": "^3.0.1", "next": "14.0.3", diff --git a/pages/api/logout.ts b/pages/api/logout.ts index f341c0f..50c42f5 100644 --- a/pages/api/logout.ts +++ b/pages/api/logout.ts @@ -1,20 +1,22 @@ -// pages/api/auth/logout.ts -import type { NextApiRequest, NextApiResponse } from "next"; +// pages/api/logout.ts -> /api/logout +import type { NextApiRequest, NextApiResponse } from 'next'; +import { serialize } from 'cookie'; export default function handler(req: NextApiRequest, res: NextApiResponse) { - const isProd = process.env.NODE_ENV === "production"; - res.setHeader( - "Set-Cookie", - [ - "token=", // empty token - "HttpOnly", - "Path=/", - "SameSite=Strict", - "Max-Age=0", // expire immediately - isProd ? "Secure" : "", - ] - .filter(Boolean) - .join("; ") - ); - return res.status(200).json({ message: "Logged out" }); + const isProd = process.env.NODE_ENV === 'production'; + + const setCookie = serialize('token', '', { + httpOnly: true, + secure: isProd, + sameSite: 'strict', // matches login + path: '/', // matches login + maxAge: 0, + expires: new Date(0), + }); + + res.setHeader('Set-Cookie', setCookie); + res.setHeader('Cache-Control', 'no-store'); + return res.status(200).json({ message: 'Logged out' }); } + +