From 837aee67fcf3de16abf8c3ef4893e83ac691caa0 Mon Sep 17 00:00:00 2001 From: Syasya Date: Fri, 15 Aug 2025 10:43:33 +0800 Subject: [PATCH] login and register flow --- .../auth/components-auth-login-form.tsx | 126 ++++++----- .../auth/components-auth-register-form.tsx | 197 +++++++++++------- pages/api/login.ts | 32 ++- pages/api/register.ts | 33 ++- 4 files changed, 247 insertions(+), 141 deletions(-) diff --git a/components/auth/components-auth-login-form.tsx b/components/auth/components-auth-login-form.tsx index 0013f4e..4ba2449 100644 --- a/components/auth/components-auth-login-form.tsx +++ b/components/auth/components-auth-login-form.tsx @@ -2,67 +2,83 @@ import IconLockDots from '@/components/icon/icon-lock-dots'; import IconMail from '@/components/icon/icon-mail'; import { useRouter } from 'next/navigation'; -import { useState } from "react"; -import axios from "axios"; +import { useState } from 'react'; +import axios from 'axios'; import toast from 'react-hot-toast'; const ComponentsAuthLoginForm = () => { - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [loading, setLoading] = useState(false) - const router = useRouter() + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const router = useRouter(); - const submitForm = async (e: React.FormEvent) => { - e.preventDefault() - - setLoading(true) - try { - const res = await axios.post(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/login`, { - email, - password, - }) - - localStorage.setItem("token", res.data.token) - - toast.success("Login successful!") - router.push("/") - } catch (err: any) { - console.error("Login error:", err) - toast.error(err.response?.data?.error || "Invalid credentials") - } finally { - setLoading(false) - } + const submitForm = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + try { + const res = await axios.post('/api/login', { email, password }); + toast.success(res.data?.message || 'Login successful!'); + router.push('/adminDashboard'); + // token cookie is already set by the server: + } catch (err: any) { + console.error('Login error:', err); + const msg = + err?.response?.data?.message || + err?.message || + 'Invalid credentials'; + toast.error(msg); + } finally { + setLoading(false); } + }; - return ( -
-
- -
- setEmail(e.target.value)} placeholder="Enter Email" className="form-input ps-10 placeholder:text-white-dark" /> - - - -
-
-
- -
- setPassword(e.target.value)} required placeholder="Enter Password" className="form-input ps-10 placeholder:text-white-dark" /> - - - -
-
- -
- ); + return ( +
+
+ +
+ setEmail(e.target.value)} + placeholder="Enter Email" + className="form-input ps-10 placeholder:text-white-dark" + required + /> + + + +
+
+
+ +
+ setPassword(e.target.value)} + required + placeholder="Enter Password" + className="form-input ps-10 placeholder:text-white-dark" + minLength={8} + /> + + + +
+
+ +
+ ); }; export default ComponentsAuthLoginForm; + diff --git a/components/auth/components-auth-register-form.tsx b/components/auth/components-auth-register-form.tsx index f5c2313..c48cfce 100644 --- a/components/auth/components-auth-register-form.tsx +++ b/components/auth/components-auth-register-form.tsx @@ -1,74 +1,131 @@ -'use client'; -import IconLockDots from '@/components/icon/icon-lock-dots'; -import IconMail from '@/components/icon/icon-mail'; -import IconUser from '@/components/icon/icon-user'; -import axios from 'axios'; -import { useRouter } from 'next/navigation'; -import { useState } from "react"; -import React from 'react'; -import toast from 'react-hot-toast'; +// components/auth/components-auth-register-form.tsx +"use client"; -const ComponentsAuthRegisterForm = () => { - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [loading, setLoading] = useState(false) - const router = useRouter() +import * as React from "react"; +import { useRouter } from "next/navigation"; - const submitForm = async(e: any) => { - e.preventDefault() - - setLoading(true) - try { - const res = await axios.post(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/register`, { - email, - password, - }) - - localStorage.setItem("token", res.data.token) - - toast.success("Register successful!") - router.push("/") - } catch (err: any) { - console.error("Register error:", err) - toast.error(err.response?.data?.error || "Something went wrong") - } finally { - setLoading(false) - } - }; - return ( -
- {/*
- -
- - - - -
-
*/} -
- -
- setEmail(e.target.value)} placeholder="Enter Email" className="form-input ps-10 placeholder:text-white-dark" /> - - - -
-
-
- -
- setPassword(e.target.value)} required placeholder="Enter Password" className="form-input ps-10 placeholder:text-white-dark" /> - - - -
-
- -
- ); +type Props = { + redirectTo?: string; // optional override }; -export default ComponentsAuthRegisterForm; +export default function ComponentsAuthRegisterForm({ redirectTo = "/dashboard" }: Props) { + const router = useRouter(); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [confirm, setConfirm] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + if (!email.trim() || !password) { + setError("Please fill in all fields."); + return; + } + if (password.length < 8) { + setError("Password must be at least 8 characters."); + return; + } + if (password !== confirm) { + setError("Passwords do not match."); + return; + } + + try { + setLoading(true); + const res = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + setError(data?.message || "Registration failed."); + return; + } + + // Cookie is set by API; just route away + router.replace(redirectTo); + } catch (err) { + setError("Network error. Please try again."); + } finally { + setLoading(false); + } + } + + return ( +
+
+ + setEmail(e.target.value)} + disabled={loading} + required + /> +
+ +
+ + setPassword(e.target.value)} + disabled={loading} + required + minLength={8} + /> +
+ +
+ + setConfirm(e.target.value)} + disabled={loading} + required + minLength={8} + /> +
+ + {error && ( +

+ {error} +

+ )} + + +
+ ); +} + diff --git a/pages/api/login.ts b/pages/api/login.ts index 2a5c6a6..8f3adff 100644 --- a/pages/api/login.ts +++ b/pages/api/login.ts @@ -1,15 +1,18 @@ -import { NextApiRequest, NextApiResponse } from "next"; +// 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 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" }); + if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" }); - const { email, password } = req.body; + 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" }); @@ -17,8 +20,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) return res.status(401).json({ message: "Invalid credentials" }); - const token = jwt.sign({ email: user.email }, SECRET_KEY, { expiresIn: "1d" }); + const token = jwt.sign({ sub: user.id, email: user.email }, SECRET_KEY, { expiresIn: "1d" }); - res.setHeader("Set-Cookie", `token=${token}; HttpOnly; Path=/; Secure`); - res.json({ token }); + 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" }); + } } + diff --git a/pages/api/register.ts b/pages/api/register.ts index 3f66d80..0ab2b75 100644 --- a/pages/api/register.ts +++ b/pages/api/register.ts @@ -1,27 +1,42 @@ -import { NextApiRequest, NextApiResponse } from "next"; +// 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 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" }); + if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" }); - const { email, password } = req.body; + 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 }, + data: { email, password: hashedPassword }, + select: { id: true, email: true, createdAt: true }, // do NOT expose password }); - const token = jwt.sign({ email: user.email }, SECRET_KEY, { expiresIn: "1d" }); + const token = jwt.sign({ sub: user.id, email: user.email }, SECRET_KEY, { expiresIn: "1d" }); - res.setHeader("Set-Cookie", `token=${token}; HttpOnly; Path=/; Secure`); - res.status(201).json({ message: "User registered", user, token }); + // 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" }); + } } +