fix auth flow

This commit is contained in:
Syasya 2025-08-20 12:41:52 +08:00
parent b8c67992cb
commit e689384977
9 changed files with 163 additions and 94 deletions

View File

@ -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=

View File

@ -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'

View File

@ -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<CrmProject[]>([]);
@ -67,6 +68,30 @@ const AdminDashboard = () => {
// near other refs
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(() => {
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 <div>Checking authentication</div>;
}
if (sitesLoading) {
return (
<DashboardLayout>

View File

@ -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 (
<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>
useEffect(() => {
let cancelled = false;
{/* 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"
/>
(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 */}
<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%)]"
return () => { cancelled = true; };
}, [router]);
if (!ready) return null; // or a small spinner if you prefer
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">
Dont have an account?{' '}
<Link
href="/register"
className="text-yellow-400 font-semibold underline transition hover:text-white"
>
{/* 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">
Dont have an account?{" "}
<Link
href="/register"
className="text-yellow-400 font-semibold underline transition hover:text-white"
>
SIGN UP
</Link>
</div>
</div>
</div>
</div>
SIGN UP
</Link>
</div>
</div>
</div>
</div>
);
};
</div>
</div>
);
}
export default LoginPage

View File

@ -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
};

View File

@ -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"] };

15
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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' });
}