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