feature/syasya/testlayout #8
11
.env.example
11
.env.example
@ -1,11 +1,6 @@
|
|||||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3005
|
FASTAPI_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'
|
||||||
|
26
Dockerfile
Normal file
26
Dockerfile
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
FROM node:18
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Accept build-time arguments
|
||||||
|
ARG FASTAPI_URL
|
||||||
|
ARG INTERNAL_API_BASE_URL
|
||||||
|
ARG DATABASE_URL
|
||||||
|
ARG JWT_SECRET
|
||||||
|
|
||||||
|
# Assign them to environment variables inside the image
|
||||||
|
ENV FASTAPI_URL=$FASTAPI_URL
|
||||||
|
ENV INTERNAL_API_BASE_URL=$INTERNAL_API_BASE_URL
|
||||||
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
ENV JWT_SECRET=$JWT_SECRET
|
||||||
|
|
||||||
|
RUN npm install --force
|
||||||
|
|
||||||
|
# Build Next.js app
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
EXPOSE 3005
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
@ -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,42 @@ const AdminDashboard = () => {
|
|||||||
// near other refs
|
// near other refs
|
||||||
const loggingRef = useRef<HTMLDivElement | null>(null);
|
const loggingRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/auth/me`, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await res.json().catch(() => null);
|
||||||
|
if (!user?.id) {
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// authenticated
|
||||||
|
} catch {
|
||||||
|
router.replace('/login');
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setAuthChecked(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [router, API]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSitesLoading(true);
|
setSitesLoading(true);
|
||||||
@ -288,7 +325,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 +337,10 @@ const AdminDashboard = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---------- RENDER ----------
|
// ---------- RENDER ----------
|
||||||
|
if (!authChecked) {
|
||||||
|
return <div>Checking authentication…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
if (sitesLoading) {
|
if (sitesLoading) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
|
@ -1,73 +1,115 @@
|
|||||||
|
// 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) => {
|
// Use ONE client-exposed API env var everywhere
|
||||||
return (
|
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
|
||||||
<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 */}
|
useEffect(() => {
|
||||||
<img
|
let cancelled = false;
|
||||||
src="/assets/images/auth/coming-soon-object1.png"
|
const controller = new AbortController();
|
||||||
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 */}
|
(async () => {
|
||||||
<div className="relative flex min-h-screen items-center justify-center px-6 py-10 sm:px-16">
|
try {
|
||||||
<div
|
const res = await fetch(`${API}/auth/me`, {
|
||||||
className="relative w-full max-w-[870px] rounded-2xl p-1
|
credentials: 'include',
|
||||||
bg-[linear-gradient(45deg,#fffbe6_0%,rgba(255,251,230,0)_25%,rgba(255,251,230,0)_75%,#fffbe6_100%)]
|
cache: 'no-store', // don't reuse a cached 401
|
||||||
dark:bg-[linear-gradient(52.22deg,#facc15_0%,rgba(250,204,21,0)_20%,rgba(250,204,21,0)_80%,#facc15_100%)]"
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (!cancelled) setReady(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await res.json().catch(() => null);
|
||||||
|
if (user?.id) {
|
||||||
|
// already logged in -> go straight to dashboard
|
||||||
|
router.replace('/adminDashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not logged in -> show form
|
||||||
|
if (!cancelled) setReady(true);
|
||||||
|
} catch {
|
||||||
|
// network/error -> show form
|
||||||
|
if (!cancelled) setReady(true);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [router, API]);
|
||||||
|
|
||||||
|
if (!ready) return null; // or a spinner/skeleton
|
||||||
|
|
||||||
|
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%)]"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ComponentsAuthLoginForm />
|
||||||
|
|
||||||
|
<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
|
|
||||||
|
@ -1,30 +1,25 @@
|
|||||||
'use client';
|
// app/layout.tsx
|
||||||
import ProviderComponent from '@/components/layouts/provider-component';
|
import type { Metadata } from "next";
|
||||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
import ProviderComponent from "@/components/layouts/provider-component";
|
||||||
import '../styles/tailwind.css';
|
import "react-perfect-scrollbar/dist/css/styles.css";
|
||||||
import { Metadata } from 'next';
|
import "../styles/tailwind.css";
|
||||||
import { Nunito } from 'next/font/google';
|
import { Nunito } from "next/font/google";
|
||||||
import { Exo_2 } from "next/font/google";
|
import { Exo_2 } from "next/font/google";
|
||||||
|
|
||||||
const exo2 = Exo_2({
|
const exo2 = Exo_2({ subsets: ["latin"], variable: "--font-exo2", weight: ["200", "400"] });
|
||||||
subsets: ["latin"],
|
const nunito = Nunito({ weight: ["400","500","600","700","800"], subsets: ["latin"], display: "swap", variable: "--font-nunito" });
|
||||||
variable: "--font-exo2",
|
|
||||||
weight: ["200", "400"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const nunito = Nunito({
|
export const metadata: Metadata = {
|
||||||
weight: ['400', '500', '600', '700', '800'],
|
title: "Rooftop Meter",
|
||||||
subsets: ['latin'],
|
icons: { icon: "/favicon.png" }, // or "/favicon.ico"
|
||||||
display: 'swap',
|
};
|
||||||
variable: '--font-nunito',
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className={`${exo2.variable} ${nunito.variable}`}>
|
||||||
<body className={exo2.variable}>
|
<body>
|
||||||
<ProviderComponent>{children}</ProviderComponent>
|
<ProviderComponent>{children}</ProviderComponent>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export interface TimeSeriesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const API_BASE_URL =
|
const API_BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://127.0.0.1:8000";
|
process.env.FASTAPI_URL ?? "http://127.0.0.1:8000";
|
||||||
|
|
||||||
export const crmapi = {
|
export const crmapi = {
|
||||||
getProjects: async () => {
|
getProjects: async () => {
|
||||||
|
@ -3,31 +3,46 @@ import IconLockDots from '@/components/icon/icon-lock-dots';
|
|||||||
import IconMail from '@/components/icon/icon-mail';
|
import IconMail from '@/components/icon/icon-mail';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
type User = { id: string; email: string; is_active: boolean };
|
||||||
|
|
||||||
const ComponentsAuthLoginForm = () => {
|
const ComponentsAuthLoginForm = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
|
||||||
|
|
||||||
const submitForm = async (e: React.FormEvent) => {
|
const submitForm = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.post('/api/login', { email, password });
|
const res = await fetch(`${API}/auth/login`, {
|
||||||
toast.success(res.data?.message || 'Login successful!');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
credentials: 'include', // cookie from FastAPI
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: any = null;
|
||||||
|
try {
|
||||||
|
data = await res.json();
|
||||||
|
} catch {
|
||||||
|
// non-JSON error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = data?.detail || data?.message || 'Invalid credentials';
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: User = data;
|
||||||
|
toast.success(`Welcome ${user.email}`);
|
||||||
router.push('/adminDashboard');
|
router.push('/adminDashboard');
|
||||||
router.refresh();
|
router.refresh();
|
||||||
// token cookie is already set by the server:
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Login error:', err);
|
toast.error(err?.message ?? 'Login failed');
|
||||||
const msg =
|
|
||||||
err?.response?.data?.message ||
|
|
||||||
err?.message ||
|
|
||||||
'Invalid credentials';
|
|
||||||
toast.error(msg);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -52,6 +67,7 @@ const ComponentsAuthLoginForm = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pb-2">
|
<div className="pb-2">
|
||||||
<label htmlFor="Password" className="text-yellow-400 text-left">Password</label>
|
<label htmlFor="Password" className="text-yellow-400 text-left">Password</label>
|
||||||
<div className="relative text-white-dark">
|
<div className="relative text-white-dark">
|
||||||
@ -70,6 +86,7 @@ const ComponentsAuthLoginForm = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -83,3 +100,4 @@ const ComponentsAuthLoginForm = () => {
|
|||||||
|
|
||||||
export default ComponentsAuthLoginForm;
|
export default ComponentsAuthLoginForm;
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,12 +33,14 @@ export default function ComponentsAuthRegisterForm({ redirectTo = "/dashboard" }
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const API = process.env.NEXT_PUBLIC_FASTAPI_URL; // e.g. http://localhost:8000
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await fetch("/api/register", {
|
const res = await fetch(`${API}/auth/register`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email, password }),
|
body: JSON.stringify({ email, password }),
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
@ -3,19 +3,18 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { IRootState } from '@/store';
|
import { IRootState } from '@/store';
|
||||||
import { toggleTheme, toggleSidebar, toggleRTL } from '@/store/themeConfigSlice';
|
import { toggleTheme, toggleSidebar } from '@/store/themeConfigSlice';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Dropdown from '@/components/dropdown';
|
import Dropdown from '@/components/dropdown';
|
||||||
import IconMenu from '@/components/icon/icon-menu';
|
import IconMenu from '@/components/icon/icon-menu';
|
||||||
import IconSun from '@/components/icon/icon-sun';
|
import IconSun from '@/components/icon/icon-sun';
|
||||||
import IconMoon from '@/components/icon/icon-moon';
|
import IconMoon from '@/components/icon/icon-moon';
|
||||||
import IconUser from '@/components/icon/icon-user';
|
import IconUser from '@/components/icon/icon-user';
|
||||||
import IconMail from '@/components/icon/icon-mail';
|
|
||||||
import IconLockDots from '@/components/icon/icon-lock-dots';
|
import IconLockDots from '@/components/icon/icon-lock-dots';
|
||||||
import IconLogout from '@/components/icon/icon-logout';
|
import IconLogout from '@/components/icon/icon-logout';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
type UserData = { id: string; email: string; createdAt: string };
|
type UserData = { id: string; email: string; is_active: boolean };
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -27,17 +26,16 @@ export default function Header() {
|
|||||||
const [user, setUser] = useState<UserData | null>(null);
|
const [user, setUser] = useState<UserData | null>(null);
|
||||||
const [loadingUser, setLoadingUser] = useState(true);
|
const [loadingUser, setLoadingUser] = useState(true);
|
||||||
|
|
||||||
// Highlight active menu (your original effect)
|
const API = process.env.NEXT_PUBLIC_FASTAPI_URL || 'http://127.0.0.1:8000';
|
||||||
|
|
||||||
|
// highlight active menu
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selector = document.querySelector(
|
const selector = document.querySelector(
|
||||||
'ul.horizontal-menu a[href="' + window.location.pathname + '"]'
|
`ul.horizontal-menu a[href="${window.location.pathname}"]`
|
||||||
);
|
);
|
||||||
if (selector) {
|
if (selector) {
|
||||||
document
|
document
|
||||||
.querySelectorAll('ul.horizontal-menu .nav-link.active')
|
.querySelectorAll('ul.horizontal-menu .nav-link.active, ul.horizontal-menu a.active')
|
||||||
.forEach((el) => el.classList.remove('active'));
|
|
||||||
document
|
|
||||||
.querySelectorAll('ul.horizontal-menu a.active')
|
|
||||||
.forEach((el) => el.classList.remove('active'));
|
.forEach((el) => el.classList.remove('active'));
|
||||||
selector.classList.add('active');
|
selector.classList.add('active');
|
||||||
const ul: any = selector.closest('ul.sub-menu');
|
const ul: any = selector.closest('ul.sub-menu');
|
||||||
@ -48,16 +46,16 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
async function loadUser() {
|
async function loadUser(signal?: AbortSignal) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch(`${API}/auth/me`, {
|
||||||
method: 'GET',
|
credentials: 'include',
|
||||||
credentials: 'include', // send cookie
|
cache: 'no-store',
|
||||||
cache: 'no-store', // avoid stale cached responses
|
signal,
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json().catch(() => null);
|
||||||
setUser(data.user);
|
setUser(data?.id ? (data as UserData) : null);
|
||||||
} catch {
|
} catch {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
} finally {
|
} finally {
|
||||||
@ -65,44 +63,53 @@ export default function Header() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoadingUser(true);
|
setLoadingUser(true);
|
||||||
loadUser();
|
const controller = new AbortController();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
loadUser(controller.signal);
|
||||||
}, [pathname]); // re-fetch on route change (after login redirect)
|
return () => controller.abort();
|
||||||
|
}, [pathname, API]);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await fetch('/api/auth/logout', { method: 'POST' });
|
try {
|
||||||
|
await fetch(`${API}/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
setUser(null);
|
setUser(null);
|
||||||
router.push('/login'); // go to login
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
|
<header className={`z-40 ${themeConfig.semidark && themeConfig.menu === 'horizontal' ? 'dark' : ''}`}>
|
||||||
<div className="shadow-sm">
|
<div className="shadow-sm">
|
||||||
<div className="relative flex w-full items-center bg-white px-5 py-2.5 dark:bg-rtgray-900">
|
<div className="relative flex w-full items-center bg-white px-5 py-2.5 dark:bg-rtgray-900">
|
||||||
{/* Logo */}
|
{/* Logo + mobile toggler */}
|
||||||
<div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
|
<div className="horizontal-logo flex items-center justify-between ltr:mr-2 rtl:ml-2 lg:hidden">
|
||||||
<div className="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12">
|
<div className="relative h-10 w-32 sm:h-11 sm:w-36 md:h-12 md:w-27 shrink-0 max-h-12">
|
||||||
<Image
|
<Image
|
||||||
src="/assets/images/newfulllogo.png"
|
src="/assets/images/newfulllogo.png"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
priority
|
priority
|
||||||
sizes="(max-width: 640px) 8rem, (max-width: 768px) 9rem, (max-width: 1024px) 10rem, 10rem"
|
sizes="(max-width: 640px) 8rem, (max-width: 768px) 9rem, (max-width: 1024px) 10rem, 10rem"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => dispatch(toggleSidebar())}
|
onClick={() => dispatch(toggleSidebar())}
|
||||||
className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700"
|
className="collapse-icon flex p-2 rounded-full hover:bg-rtgray-200 dark:text-white dark:hover:bg-rtgray-700"
|
||||||
>
|
>
|
||||||
<IconMenu className="h-6 w-6" />
|
<IconMenu className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Right-side actions */}
|
{/* Right-side actions */}
|
||||||
<div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] lg:space-x-2">
|
<div className="flex items-center justify-end space-x-1.5 ltr:ml-auto rtl:mr-auto rtl:space-x-reverse dark:text-[#d0d2d6] lg:space-x-2">
|
||||||
@ -124,21 +131,21 @@ useEffect(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User dropdown */}
|
{/* User dropdown */}
|
||||||
<div className="dropdown flex shrink-0 ">
|
<div className="dropdown flex shrink-0">
|
||||||
{loadingUser ? (
|
{loadingUser ? (
|
||||||
<div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" />
|
<div className="h-9 w-9 rounded-full animate-pulse bg-gray-300 dark:bg-rtgray-800" />
|
||||||
) : user ? (
|
) : user ? (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
placement={isRtl ? 'bottom-start' : 'bottom-end'}
|
placement={isRtl ? 'bottom-start' : 'bottom-end'}
|
||||||
btnClassName="relative group block"
|
btnClassName="relative group block"
|
||||||
panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2" // ✅
|
panelClassName="rounded-lg shadow-lg border border-white/10 bg-rtgray-100 dark:bg-rtgray-800 p-2"
|
||||||
button={
|
button={
|
||||||
<div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700">
|
<div className="h-9 w-9 rounded-full bg-rtgray-200 dark:bg-rtgray-800 flex items-center justify-center group-hover:bg-rtgray-300 dark:group-hover:bg-rtgray-700">
|
||||||
<IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
<IconUser className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ul className="w-[230px] font-semibold text-dark"> {/* make sure this stays transparent */}
|
<ul className="w-[230px] font-semibold text-dark">
|
||||||
<li className="px-4 py-4 flex items-center">
|
<li className="px-4 py-4 flex items-center">
|
||||||
<div className="truncate ltr:pl-1.5 rtl:pr-4">
|
<div className="truncate ltr:pl-1.5 rtl:pr-4">
|
||||||
<h4 className="text-sm text-left">{user.email}</h4>
|
<h4 className="text-sm text-left">{user.email}</h4>
|
||||||
|
@ -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,46 +0,0 @@
|
|||||||
// pages/api/auth/me.ts
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const SECRET_KEY = process.env.JWT_SECRET as string;
|
|
||||||
|
|
||||||
function readCookieToken(req: NextApiRequest) {
|
|
||||||
const cookie = req.headers.cookie || "";
|
|
||||||
const match = cookie.split("; ").find((c) => c.startsWith("token="));
|
|
||||||
return match?.split("=")[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAuthBearer(req: NextApiRequest) {
|
|
||||||
const auth = req.headers.authorization;
|
|
||||||
if (!auth?.startsWith("Bearer ")) return undefined;
|
|
||||||
return auth.slice("Bearer ".length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasEmail(payload: string | JwtPayload): payload is JwtPayload & { email: string } {
|
|
||||||
return typeof payload === "object" && payload !== null && typeof (payload as any).email === "string";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
if (req.method !== "GET") return res.status(405).json({ message: "Method not allowed" });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = readAuthBearer(req) ?? readCookieToken(req);
|
|
||||||
if (!token) return res.status(401).json({ message: "Unauthorized" });
|
|
||||||
|
|
||||||
const decoded = jwt.verify(token, SECRET_KEY);
|
|
||||||
if (!hasEmail(decoded)) return res.status(401).json({ message: "Invalid token" });
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { email: decoded.email },
|
|
||||||
select: { id: true, email: true, createdAt: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return res.status(401).json({ message: "User not found" });
|
|
||||||
return res.status(200).json({ user });
|
|
||||||
} catch {
|
|
||||||
return res.status(401).json({ message: "Invalid token" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
|||||||
// pages/api/login.ts
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const SECRET_KEY = process.env.JWT_SECRET as string;
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { email, password } = req.body as { email?: string; password?: string };
|
|
||||||
if (!email || !password) return res.status(400).json({ message: "Email and password are required" });
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { email } });
|
|
||||||
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
|
||||||
|
|
||||||
const isMatch = await bcrypt.compare(password, user.password);
|
|
||||||
if (!isMatch) return res.status(401).json({ message: "Invalid credentials" });
|
|
||||||
|
|
||||||
const token = jwt.sign({ sub: String(user.id), email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === "production";
|
|
||||||
const cookie = [
|
|
||||||
`token=${token}`,
|
|
||||||
"HttpOnly",
|
|
||||||
"Path=/",
|
|
||||||
"SameSite=Strict",
|
|
||||||
`Max-Age=${60 * 60 * 24}`, // 1 day
|
|
||||||
isProd ? "Secure" : "", // only secure in prod
|
|
||||||
].filter(Boolean).join("; ");
|
|
||||||
|
|
||||||
res.setHeader("Set-Cookie", cookie);
|
|
||||||
return res.status(200).json({ message: "Login successful" });
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
return res.status(500).json({ message: "Something went wrong" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
// pages/api/auth/logout.ts
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
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" });
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// pages/api/register.ts
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
const SECRET_KEY = process.env.JWT_SECRET as string;
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { email, password } = req.body as { email?: string; password?: string };
|
|
||||||
|
|
||||||
if (!email || !password) return res.status(400).json({ message: "Email and password are required" });
|
|
||||||
|
|
||||||
const existingUser = await prisma.user.findUnique({ where: { email } });
|
|
||||||
if (existingUser) return res.status(400).json({ message: "User already exists" });
|
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
const user = await prisma.user.create({
|
|
||||||
data: { email, password: hashedPassword },
|
|
||||||
select: { id: true, email: true, createdAt: true }, // do NOT expose password
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = jwt.sign({ sub: String(user.id), email: user.email }, SECRET_KEY, { expiresIn: "1d" });
|
|
||||||
|
|
||||||
// Set a secure, httpOnly cookie
|
|
||||||
const maxAge = 60 * 60 * 24; // 1 day
|
|
||||||
res.setHeader(
|
|
||||||
"Set-Cookie",
|
|
||||||
`token=${token}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict; Secure`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(201).json({ message: "User registered", user });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).json({ message: "Something went wrong" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
18
prisma/migrations/20250815012144_init_users/migration.sql
Normal file
18
prisma/migrations/20250815012144_init_users/migration.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `EnergyData` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the `Site` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "public"."EnergyData" DROP CONSTRAINT "EnergyData_consumptionSiteId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "public"."EnergyData" DROP CONSTRAINT "EnergyData_generationSiteId_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "public"."EnergyData";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "public"."Site";
|
@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `password` on the `User` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `passwordHash` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."User" DROP COLUMN "password",
|
||||||
|
ADD COLUMN "name" TEXT,
|
||||||
|
ADD COLUMN "passwordHash" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
14
prisma/migrations/20250815022446_add_user/migration.sql
Normal file
14
prisma/migrations/20250815022446_add_user/migration.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `name` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `passwordHash` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `updatedAt` on the `User` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."User" DROP COLUMN "name",
|
||||||
|
DROP COLUMN "passwordHash",
|
||||||
|
DROP COLUMN "updatedAt",
|
||||||
|
ADD COLUMN "password" TEXT NOT NULL;
|
@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (e.g., Git)
|
# It should be added in your version-control system (e.g., Git)
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 78 KiB |
Loading…
x
Reference in New Issue
Block a user