Compare commits
7 Commits
f047a9ec1c
...
b7d144bb66
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7d144bb66 | ||
![]() |
753abe7607 | ||
![]() |
8ee42ea0ac | ||
![]() |
a4c0abc10f | ||
![]() |
021695c5b1 | ||
![]() |
3b9ee4dd5b | ||
![]() |
2c1938e62a |
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
NEXT_PUBLIC_CHINT_TOKEN=lIywwAMdrOdsRxuWvRoekdxrPtmIPkxA
|
||||||
|
DATABASE_URL="postgresql://postgres:root@localhost:5432/rooftop?schema=public"
|
||||||
|
JWT_SECRET="secret_key"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,3 +34,5 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
.env
|
||||||
|
@ -64,34 +64,6 @@ const InverterViewPage = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
|
||||||
Event
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
<Tab as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
<Tab as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
|
||||||
Firmware
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
<Tab as={Fragment}>
|
|
||||||
{({ selected }) => (
|
|
||||||
<button className={`${selected ? 'border-b !border-primary text-primary !outline-none' : ''} -mb-[1px] flex items-center border-transparent p-5 py-3 before:inline-block hover:border-b hover:!border-primary hover:text-primary`} >
|
|
||||||
Data
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Tab>
|
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel>
|
<Tab.Panel>
|
||||||
@ -118,9 +90,8 @@ const InverterViewPage = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>Chart</Tab.Panel>
|
||||||
</Tab.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
)}
|
)}
|
||||||
|
@ -100,12 +100,30 @@ const Sidebar = () => {
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li className="menu nav-item">
|
<li className="menu nav-item">
|
||||||
<Link href="#" className="nav-link group">
|
<button type="button" className={`${currentMenu === 'sungrow' ? 'active' : ''} nav-link group w-full`} onClick={() => toggleMenu('sungrow')}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
|
<IconMenuComponents className="shrink-0 group-hover:!text-primary" />
|
||||||
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Sungrow</span>
|
<span className="text-black ltr:pl-3 rtl:pr-3 dark:text-[#506690] dark:group-hover:text-white-dark">Sungrow</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
|
||||||
|
<div className={currentMenu !== 'component' ? '-rotate-90 rtl:rotate-90' : ''}>
|
||||||
|
<IconCaretDown />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimateHeight duration={300} height={currentMenu === 'sungrow' ? 'auto' : 0}>
|
||||||
|
<ul className="sub-menu text-gray-500">
|
||||||
|
<li>
|
||||||
|
<Link href="/sungrow/plant">Plant</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/sungrow/device">Device</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/sungrow/maintenance">Maintenance</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</AnimateHeight>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li className="menu nav-item">
|
<li className="menu nav-item">
|
||||||
|
20
http/auth.http
Normal file
20
http/auth.http
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
GET http://localhost:3005/api/auth/me
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2YmM2MGIzMC1hNzcyLTRiM2MtOTYwOC1jMjgyNmNhMjA2NjEiLCJlbWFpbCI6ImFzZEBhc2QuY29tIiwiaWF0IjoxNzQwNTUyMjE4LCJleHAiOjE3NDA1NTU4MTh9.n3rFlFbqKY-kg8YF6cuRkT_Kc6hsCqQQ87TJbkS8DAg
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://localhost:3005/api/register
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "asd@asd.com",
|
||||||
|
"password":"123456"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
POST http://localhost:3005/api/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"email": "asd@asd.com",
|
||||||
|
"password":"123456"
|
||||||
|
}
|
19
middleware.ts
Normal file
19
middleware.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
const SECRET_KEY = process.env.JWT_SECRET as string;
|
||||||
|
|
||||||
|
export function middleware(req: NextRequest) {
|
||||||
|
const token = req.cookies.get("token")?.value;
|
||||||
|
|
||||||
|
if (!token) return NextResponse.redirect(new URL("/login", req.url));
|
||||||
|
|
||||||
|
try {
|
||||||
|
jwt.verify(token, SECRET_KEY);
|
||||||
|
return NextResponse.next();
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.redirect(new URL("/login", req.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = { matcher: ["/dashboard", "/profile"] };
|
893
package-lock.json
generated
893
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.10.6",
|
"@emotion/react": "^11.10.6",
|
||||||
"@headlessui/react": "^1.7.8",
|
"@headlessui/react": "^1.7.8",
|
||||||
|
"@prisma/client": "^6.4.1",
|
||||||
"@reduxjs/toolkit": "^1.9.1",
|
"@reduxjs/toolkit": "^1.9.1",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
@ -18,9 +19,12 @@
|
|||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.32.0",
|
||||||
"eslint-config-next": "13.1.2",
|
"eslint-config-next": "13.1.2",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^22.4.10",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
"ni18n": "^1.0.5",
|
"ni18n": "^1.0.5",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@ -31,7 +35,6 @@
|
|||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
"typescript": "4.9.4",
|
|
||||||
"universal-cookie": "^6.1.1",
|
"universal-cookie": "^6.1.1",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
@ -44,6 +47,7 @@
|
|||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
pages/api/auth/me.ts
Normal file
27
pages/api/auth/me.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const SECRET_KEY = process.env.JWT_SECRET as string;
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1]; // Extract token
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded: any = jwt.verify(token, SECRET_KEY);
|
||||||
|
const user = await prisma.user.findUnique({ where: { id: decoded.userId } });
|
||||||
|
|
||||||
|
if (!user) return res.status(401).json({ message: "User not found" });
|
||||||
|
|
||||||
|
res.json({ user });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(401).json({ message: "Invalid token" });
|
||||||
|
}
|
||||||
|
}
|
24
pages/api/login.ts
Normal file
24
pages/api/login.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { 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" });
|
||||||
|
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
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({ userId: user.id, email: user.email }, SECRET_KEY, { expiresIn: "1h" });
|
||||||
|
|
||||||
|
res.setHeader("Set-Cookie", `token=${token}; HttpOnly; Path=/; Secure`);
|
||||||
|
res.json({ token });
|
||||||
|
}
|
21
pages/api/register.ts
Normal file
21
pages/api/register.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" });
|
||||||
|
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
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 },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({ message: "User registered", user });
|
||||||
|
}
|
12
prisma/migrations/20250226061632_init/migration.sql
Normal file
12
prisma/migrations/20250226061632_init/migration.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "postgresql"
|
22
prisma/schema.prisma
Normal file
22
prisma/schema.prisma
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
email String @unique
|
||||||
|
password String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user