feature/syasya/testlayout #7
@ -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;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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