login and register flow
This commit is contained in:
parent
44bb94ded8
commit
837aee67fc
@ -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>
|
||||
<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" />
|
||||
<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>
|
||||
@ -57,12 +72,13 @@ const ComponentsAuthLoginForm = () => {
|
||||
<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"
|
||||
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;
|
||||
|
||||
|
@ -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()
|
||||
type Props = {
|
||||
redirectTo?: string; // optional override
|
||||
};
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await axios.post(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/register`, {
|
||||
email,
|
||||
password,
|
||||
})
|
||||
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);
|
||||
|
||||
localStorage.setItem("token", res.data.token)
|
||||
async function onSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
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)
|
||||
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;
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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" });
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user