login and register flow

This commit is contained in:
Syasya 2025-08-15 10:43:33 +08:00
parent 44bb94ded8
commit 837aee67fc
4 changed files with 247 additions and 141 deletions

View File

@ -2,53 +2,68 @@
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)
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("/")
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)
toast.error(err.response?.data?.error || "Invalid credentials")
console.error('Login error:', err);
const msg =
err?.response?.data?.message ||
err?.message ||
'Invalid credentials';
toast.error(msg);
} finally {
setLoading(false)
}
setLoading(false);
}
};
return (
<form className="space-y-3 dark:text-white" onSubmit={submitForm}>
<div>
<label htmlFor="Email" className='text-yellow-400 text-left'>Email</label>
<label htmlFor="Email" className="text-yellow-400 text-left">Email</label>
<div className="relative text-white-dark">
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter Email" className="form-input ps-10 placeholder:text-white-dark" />
<input
id="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter Email"
className="form-input ps-10 placeholder:text-white-dark"
required
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconMail fill={true} />
</span>
</div>
</div>
<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">
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required placeholder="Enter Password" className="form-input ps-10 placeholder:text-white-dark" />
<input
id="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
placeholder="Enter Password"
className="form-input ps-10 placeholder:text-white-dark"
minLength={8}
/>
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
@ -59,10 +74,11 @@ const ComponentsAuthLoginForm = () => {
disabled={loading}
className="w-full uppercase border-0 rounded-md bg-[#fcd913] text-black font-semibold py-2 hover:bg-[#E6C812] transition disabled:opacity-70"
>
{loading ? "Logging in..." : "Sign In"}
{loading ? 'Logging in...' : 'Sign In'}
</button>
</form>
);
};
export default ComponentsAuthLoginForm;

View File

@ -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)
}
type Props = {
redirectTo?: string; // optional override
};
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<string | null>(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 (
<form className="space-y-5 dark:text-white" onSubmit={submitForm}>
{/* <div>
<label htmlFor="Name">Name</label>
<div className="relative text-white-dark">
<input id="Name" type="text" placeholder="Enter Name" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconUser fill={true} />
</span>
</div>
</div> */}
<form onSubmit={onSubmit} className="space-y-4 text-left">
<div>
<label htmlFor="Email" className='text-yellow-400 text-left'>Email</label>
<div className="relative text-white-dark">
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Enter Email" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconMail fill={true} />
</span>
<label htmlFor="email" className="mb-1 block text-sm text-gray-300">
Email
</label>
<input
id="email"
type="email"
autoComplete="email"
className="w-full rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder-gray-400 outline-none focus:border-yellow-400"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
required
/>
</div>
<div>
<label htmlFor="password" className="mb-1 block text-sm text-gray-300">
Password
</label>
<input
id="password"
type="password"
autoComplete="new-password"
className="w-full rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder-gray-400 outline-none focus:border-yellow-400"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
required
minLength={8}
/>
</div>
<div className= "pb-2">
<label htmlFor="Password" className='text-yellow-400 text-left'>Password</label>
<div className="relative text-white-dark">
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required placeholder="Enter Password" className="form-input ps-10 placeholder:text-white-dark" />
<span className="absolute start-4 top-1/2 -translate-y-1/2">
<IconLockDots fill={true} />
</span>
<div>
<label htmlFor="confirm" className="mb-1 block text-sm text-gray-300">
Confirm Password
</label>
<input
id="confirm"
type="password"
autoComplete="new-password"
className="w-full rounded-xl border border-white/10 bg-white/10 px-4 py-3 text-white placeholder-gray-400 outline-none focus:border-yellow-400"
placeholder="••••••••"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
disabled={loading}
required
minLength={8}
/>
</div>
</div>
<button type="submit" disabled={loading} className=" w-full uppercase border-0 rounded-md bg-[#fcd913] text-black font-semibold py-2 hover:bg-[#E6C812] transition disabled:opacity-70">
{loading ? "Creating account..." : "Register"}
{error && (
<p className="rounded-lg bg-red-500/10 px-3 py-2 text-sm text-red-300">
{error}
</p>
)}
<button
type="submit"
disabled={loading}
className="inline-flex w-full items-center justify-center rounded-xl bg-yellow-400 px-4 py-3 font-semibold text-black hover:brightness-90 disabled:opacity-60"
>
{loading ? "Creating account…" : "Create account"}
</button>
</form>
);
};
}
export default ComponentsAuthRegisterForm;

View File

@ -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" });
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" });
}
}

View File

@ -1,16 +1,19 @@
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" });
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" });
@ -18,10 +21,22 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
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({ 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" });
}
}