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 | ||||
| *.tsbuildinfo | ||||
| next-env.d.ts | ||||
| 
 | ||||
| .env | ||||
|  | ||||
| @ -64,34 +64,6 @@ const InverterViewPage = (props: Props) => { | ||||
|                                     </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`} > | ||||
|                                         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.Panels> | ||||
|                             <Tab.Panel> | ||||
| @ -118,9 +90,8 @@ const InverterViewPage = (props: Props) => { | ||||
|                                     </div> | ||||
| 
 | ||||
|                                 </div> | ||||
| 
 | ||||
| 
 | ||||
|                             </Tab.Panel> | ||||
|                             <Tab.Panel>Chart</Tab.Panel> | ||||
|                         </Tab.Panels> | ||||
|                     </Tab.Group> | ||||
|                 )} | ||||
|  | ||||
| @ -100,12 +100,30 @@ const Sidebar = () => { | ||||
|                             </li> | ||||
| 
 | ||||
|                             <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"> | ||||
|                                         <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> | ||||
|                                     </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 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": { | ||||
|         "@emotion/react": "^11.10.6", | ||||
|         "@headlessui/react": "^1.7.8", | ||||
|         "@prisma/client": "^6.4.1", | ||||
|         "@reduxjs/toolkit": "^1.9.1", | ||||
|         "@tippyjs/react": "^4.2.6", | ||||
|         "@types/node": "18.11.18", | ||||
| @ -18,9 +19,12 @@ | ||||
|         "@types/react-dom": "18.0.10", | ||||
|         "apexcharts": "^4.5.0", | ||||
|         "axios": "^1.7.9", | ||||
|         "bcrypt": "^5.1.1", | ||||
|         "cookie": "^1.0.2", | ||||
|         "eslint": "8.32.0", | ||||
|         "eslint-config-next": "13.1.2", | ||||
|         "i18next": "^22.4.10", | ||||
|         "jsonwebtoken": "^9.0.2", | ||||
|         "next": "14.0.3", | ||||
|         "ni18n": "^1.0.5", | ||||
|         "react": "18.2.0", | ||||
| @ -31,7 +35,6 @@ | ||||
|         "react-perfect-scrollbar": "^1.5.8", | ||||
|         "react-popper": "^2.3.0", | ||||
|         "react-redux": "^8.1.3", | ||||
|         "typescript": "4.9.4", | ||||
|         "universal-cookie": "^6.1.1", | ||||
|         "yup": "^0.32.11" | ||||
|     }, | ||||
| @ -44,6 +47,7 @@ | ||||
|         "postcss": "^8.4.35", | ||||
|         "prettier": "^2.8.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