diff --git a/payment-iq/app/api/auth/login/route.tsx b/payment-iq/app/api/auth/login/route.tsx new file mode 100644 index 0000000..644f427 --- /dev/null +++ b/payment-iq/app/api/auth/login/route.tsx @@ -0,0 +1,53 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +// This is your POST handler for the login endpoint +export async function POST(request: Request) { + try { + const { email, password } = await request.json(); + + // --- Replace with your ACTUAL authentication logic --- + // In a real application, you would: + // 1. Query your database for the user by email. + // 2. Hash the provided password and compare it to the stored hashed password. + // 3. If credentials match, generate a secure JWT (JSON Web Token) or session ID. + // 4. Store the token/session ID securely (e.g., in a database or Redis). + + // Mock authentication for demonstration purposes: + if (email === "admin@example.com" && password === "password123") { + const authToken = "mock-jwt-token-12345"; // Replace with a real, securely generated token + + // Set the authentication token as an HTTP-only cookie + // HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript, + // which mitigates XSS attacks. + ( + await // Set the authentication token as an HTTP-only cookie + // HTTP-only cookies are crucial for security as they cannot be accessed by client-side JavaScript, + // which mitigates XSS attacks. + cookies() + ).set("auth_token", authToken, { + httpOnly: true, // IMPORTANT: Makes the cookie inaccessible to client-side scripts + secure: process.env.NODE_ENV === "production", // Use secure in production (HTTPS) + maxAge: 60 * 60 * 24 * 7, // 1 week + path: "/", // Available across the entire site + sameSite: "lax", // Protects against CSRF + }); + + return NextResponse.json( + { success: true, message: "Login successful" }, + { status: 200 } + ); + } else { + return NextResponse.json( + { success: false, message: "Invalid credentials" }, + { status: 401 } + ); + } + } catch (error) { + console.error("Login API error:", error); + return NextResponse.json( + { success: false, message: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/payment-iq/app/components/PageLinks/PageLinks.tsx b/payment-iq/app/components/PageLinks/PageLinks.tsx index d90e5f9..f2ef968 100644 --- a/payment-iq/app/components/PageLinks/PageLinks.tsx +++ b/payment-iq/app/components/PageLinks/PageLinks.tsx @@ -1,25 +1,33 @@ +// app/components/PageLinks/PageLinks.tsx "use client"; import Link from "next/link"; -import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; +import { ISidebarLink } from "@/app/features/dashboard/sidebar/SidebarLink.interfaces"; // Keep this import import clsx from "clsx"; // Utility to merge class names -import "./PageLinks.scss"; +import "./PageLinks.scss"; // Keep this import +// Define the props interface for your PageLinks component +// It now extends ISidebarLink and includes isShowIcon interface IPageLinksProps extends ISidebarLink { isShowIcon?: boolean; } +// PageLinks component export default function PageLinks({ title, path, - icon: Icon, -}: IPageLinksProps) { + icon: Icon, // Destructure icon as Icon +}: // isShowIcon, // If you plan to use this prop, uncomment it and add logic +IPageLinksProps) { return ( - - - {Icon && } - {title} - + // Corrected Link usage for Next.js 13/14 App Router: + // - Removed `passHref` and `legacyBehavior` + // - Applied `className` directly to the Link component + // - Removed the nested `` tag + + {/* Conditionally render Icon if it exists */} + {Icon && } + {title} ); } diff --git a/payment-iq/app/dashboard/admin/users/page.tsx b/payment-iq/app/dashboard/admin/users/page.tsx index de75e47..24a6ad9 100644 --- a/payment-iq/app/dashboard/admin/users/page.tsx +++ b/payment-iq/app/dashboard/admin/users/page.tsx @@ -5,7 +5,7 @@ export default async function BackOfficeUsersPage() { process.env.NEXT_PUBLIC_BASE_URL || process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000"; - const res = await fetch(`${baseUrl}ss/api/dashboard/admin/users`, { + const res = await fetch(`${baseUrl}/api/dashboard/admin/users`, { cache: "no-store", // 👈 disables caching for SSR freshness }); const users = await res.json(); diff --git a/payment-iq/app/features/Auth/LoginModal.scss b/payment-iq/app/features/Auth/LoginModal.scss new file mode 100644 index 0000000..4a115a4 --- /dev/null +++ b/payment-iq/app/features/Auth/LoginModal.scss @@ -0,0 +1,197 @@ +/* app/styles/LoginModal.scss (BEM Methodology) */ + +// Variables for consistent styling +$primary-color: #2563eb; // Blue-600 equivalent +$primary-hover-color: #1d4ed8; // Blue-700 equivalent +$success-color: #16a34a; // Green-600 equivalent +$error-color: #dc2626; // Red-600 equivalent +$text-color-dark: #1f2937; // Gray-800 equivalent +$text-color-medium: #4b5563; // Gray-700 equivalent +$text-color-light: #6b7280; // Gray-600 equivalent +$border-color: #d1d5db; // Gray-300 equivalent +$bg-color-light: #f3f4f6; // Gray-100 equivalent +$bg-color-white: #ffffff; + +/* --- Login Modal Block (.login-modal) --- */ +.login-modal__overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(17, 24, 39, 0.75); // Gray-900 75% opacity + display: flex; + align-items: center; + justify-content: center; + z-index: 50; + padding: 1rem; // p-4 +} + +.login-modal__content { + background-color: $bg-color-white; + border-radius: 0.75rem; // rounded-xl + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); // shadow-2xl + padding: 2rem; // p-8 + width: 100%; + max-width: 28rem; // max-w-md + transform: scale(1); + opacity: 1; + transition: all 0.3s ease-in-out; // transition-all duration-300 +} + +.login-modal__title { + font-size: 1.875rem; // text-3xl + font-weight: 700; // font-bold + color: $text-color-dark; + margin-bottom: 1.5rem; // mb-6 + text-align: center; +} + +/* --- Login Form Block (.login-form) --- */ +.login-form { + display: flex; + flex-direction: column; + gap: 1.5rem; // space-y-6 +} + +.login-form__group { + // No specific styles needed here, just a container for label/input +} + +.login-form__label { + display: block; + font-size: 0.875rem; // text-sm + font-weight: 500; // font-medium + color: $text-color-medium; + margin-bottom: 0.25rem; // mb-1 +} + +.login-form__input { + display: block; + width: 100%; + padding: 0.5rem 1rem; // px-4 py-2 + border: 1px solid $border-color; + border-radius: 0.5rem; // rounded-lg + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm + font-size: 0.875rem; // sm:text-sm + &:focus { + outline: 2px solid transparent; + outline-offset: 2px; + border-color: $primary-color; // focus:border-blue-500 + box-shadow: 0 0 0 1px $primary-color, 0 0 0 3px rgba($primary-color, 0.5); // focus:ring-blue-500 + } + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.login-form__message { + text-align: center; + font-size: 0.875rem; // text-sm + font-weight: 500; // font-medium +} + +.login-form__message--success { + color: $success-color; +} + +.login-form__message--error { + color: $error-color; +} + +.login-form__button { + width: 100%; + display: flex; + justify-content: center; + padding: 0.75rem 1rem; // py-3 px-4 + border: 1px solid transparent; + border-radius: 0.5rem; // rounded-lg + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-sm + font-size: 1.125rem; // text-lg + font-weight: 600; // font-semibold + color: $bg-color-white; + background-color: $primary-color; + cursor: pointer; + transition: background-color 0.3s ease-in-out, box-shadow 0.3s ease-in-out; // transition duration-300 ease-in-out + &:hover { + background-color: darken($primary-color, 5%); // blue-700 equivalent + } + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5), + 0 0 0 4px rgba($primary-color, 0.5); // focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 + } + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.login-form__spinner { + animation: spin 1s linear infinite; + height: 1.25rem; // h-5 + width: 1.25rem; // w-5 + color: white; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* --- Page Container Block (.page-container) --- */ +.page-container { + min-height: 100vh; + background-color: $bg-color-light; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: "Inter", sans-serif; // Assuming Inter font is used + padding: 1rem; +} + +.page-container__content { + width: 100%; + max-width: 56rem; // max-w-4xl + background-color: $bg-color-white; + border-radius: 0.75rem; // rounded-xl + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg + padding: 2rem; // p-8 + text-align: center; +} + +.page-container__title { + font-size: 2.25rem; // text-4xl + font-weight: 700; // font-bold + color: $text-color-dark; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__message--logged-in { + font-size: 1.25rem; // text-xl + color: $success-color; + margin-bottom: 1rem; // mb-4 +} + +.page-container__text { + color: $text-color-medium; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__button--logout { + padding: 0.75rem 1.5rem; // px-6 py-3 + background-color: $error-color; + color: $bg-color-white; + font-weight: 600; // font-semibold + border-radius: 0.5rem; // rounded-lg + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md + transition: background-color 0.3s ease-in-out; +} diff --git a/payment-iq/app/features/Auth/LoginModal.tsx b/payment-iq/app/features/Auth/LoginModal.tsx new file mode 100644 index 0000000..e2025a9 --- /dev/null +++ b/payment-iq/app/features/Auth/LoginModal.tsx @@ -0,0 +1,112 @@ +"use client"; // This MUST be the very first line of the file + +import React, { useState, useEffect } from "react"; +import "./LoginModal.scss"; // Adjust path based on your actual structure + +// Define the props interface for LoginModal +type LoginModalProps = { + onLogin: (email: string, password: string) => Promise; + authMessage: string; + clearAuthMessage: () => void; +}; +// LoginModal component +export default function LoginModal({ + onLogin, + authMessage, + clearAuthMessage, +}: LoginModalProps) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + console.log("LoginModal rendered"); // Debugging log to check if the component renders + + // Effect to clear authentication messages when email or password inputs change + useEffect(() => { + // clearAuthMessage(); + }, [ + email, + password, + // clearAuthMessage + ]); // Dependency array ensures effect runs when these change + + // Handler for form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); // Prevent default form submission behavior + setIsLoading(true); // Set loading state to true + await onLogin(email, password); // Call the passed onLogin function (now uncommented) + setIsLoading(false); // Set loading state back to false after login attempt + }; + return ( + // The content of the login modal, without the modal overlay/wrapper +
+ {/* Email input field */} +
+ + setEmail(e.target.value)} + required + disabled={isLoading} + /> +
+ + {/* Password input field */} +
+ + setPassword(e.target.value)} + required + disabled={isLoading} + /> +
+ + +
+ ); +} diff --git a/payment-iq/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx b/payment-iq/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx index d36e1ad..cea093d 100644 --- a/payment-iq/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx +++ b/payment-iq/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx @@ -11,6 +11,7 @@ import { TransactionsOverView } from "../../TransactionsOverview/TransactionsOve export const DashboardHomePage = () => { return ( <> + {/* Conditional rendering of the Generic Modal, passing LoginModal as children */} diff --git a/payment-iq/app/login/page.scss b/payment-iq/app/login/page.scss new file mode 100644 index 0000000..2e32ae3 --- /dev/null +++ b/payment-iq/app/login/page.scss @@ -0,0 +1,64 @@ +// Variables for consistent styling +$primary-color: #2563eb; // Blue-600 equivalent +$primary-hover-color: #1d4ed8; // Blue-700 equivalent +$success-color: #16a34a; // Green-600 equivalent +$error-color: #dc2626; // Red-600 equivalent +$text-color-dark: #1f2937; // Gray-800 equivalent +$text-color-medium: #4b5563; // Gray-700 equivalent +$text-color-light: #6b7280; // Gray-600 equivalent +$border-color: #d1d5db; // Gray-300 equivalent +$bg-color-light: #f3f4f6; // Gray-100 equivalent +$bg-color-white: #ffffff; + +.page-container { + min-height: 100vh; + background-color: $bg-color-light; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-family: "Inter", sans-serif; // Assuming Inter font is used + padding: 1rem; +} + +.page-container__content { + width: 100%; + max-width: 56rem; // max-w-4xl + background-color: $bg-color-white; + border-radius: 0.75rem; // rounded-xl + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); // shadow-lg + padding: 2rem; // p-8 + text-align: center; +} + +.page-container__title { + font-size: 2.25rem; // text-4xl + font-weight: 700; // font-bold + color: $text-color-dark; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__message--logged-in { + font-size: 1.25rem; // text-xl + color: $success-color; + margin-bottom: 1rem; // mb-4 +} + +.page-container__text { + color: $text-color-medium; + margin-bottom: 1.5rem; // mb-6 +} + +.page-container__button--logout { + padding: 0.75rem 1.5rem; // px-6 py-3 + background-color: $error-color; + color: $bg-color-white; + font-weight: 600; // font-semibold + border-radius: 0.5rem; // rounded-lg + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); // shadow-md + transition: background-color 0.3s ease-in-out; + &:hover { + background-color: darken($error-color, 5%); // red-700 equivalent + } +} diff --git a/payment-iq/app/login/page.tsx b/payment-iq/app/login/page.tsx new file mode 100644 index 0000000..c185f15 --- /dev/null +++ b/payment-iq/app/login/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import LoginModal from "../features/Auth/LoginModal"; // Your LoginModal component + +import "./page.scss"; // Global styles for LoginModal and page +import Modal from "../components/Modal/Modal"; + +export default function LoginPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const redirectPath = searchParams.get("redirect") || "/dashboard"; + + const [authMessage, setAuthMessage] = useState(""); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + // Check if already logged in by trying to fetch a protected resource or checking a client-accessible flag + // For HTTP-only cookies, you can't directly read the token here. + // Instead, you'd rely on a server-side check (e.g., in middleware or a Server Component) + // or a simple client-side flag if your backend also sets one (less secure for token itself). + // For this example, we'll assume if they land here, they need to log in. + // A more robust check might involve a quick API call to /api/auth/status + // if the token is in an HTTP-only cookie. + const checkAuthStatus = async () => { + // In a real app, this might be a call to a /api/auth/status endpoint + // that checks the HTTP-only cookie on the server and returns a boolean. + // For now, we'll rely on the middleware to redirect if unauthenticated. + // If the user somehow lands on /login with a valid cookie, the middleware + // should have redirected them already. + }; + checkAuthStatus(); + }, []); + + const handleLogin = async (email: string, password: string) => { + setAuthMessage("Attempting login..."); + try { + const response = await fetch("/api/auth/login", { + // <--- CALLING YOUR INTERNAL ROUTE HANDLER + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (response.ok) { + // Check if the response status is 2xx + // Backend has successfully set the HTTP-only cookie + setAuthMessage("Login successful!"); + setIsLoggedIn(true); + // Redirect to the intended path after successful login + router.replace(redirectPath); + } else { + // Handle login errors (e.g., invalid credentials) + setAuthMessage(data.message || "Login failed. Please try again."); + setIsLoggedIn(false); + } + } catch (error) { + console.error("Login failed:", error); + setAuthMessage("An error occurred during login. Please try again later."); + setIsLoggedIn(false); + } + return isLoggedIn; // Return the current login status + }; + + const clearAuthMessage = () => setAuthMessage(""); + + // If user is already logged in (e.g., redirected by middleware to dashboard), + // this page shouldn't be visible. The middleware should handle the primary redirect. + // This `isLoggedIn` state here is more for internal page logic if the user somehow + // bypasses middleware or lands on /login with a valid session. + // For a robust setup, the middleware is key. + + return ( +
+
+

Payment Backoffice

+

+ Please log in to access the backoffice. +

+
+ + {/* Always show the modal on the login page */} + { + /* No direct close for login modal, user must log in */ + }} + title="Login to Backoffice" + > + + +
+ ); +} diff --git a/payment-iq/app/page.tsx b/payment-iq/app/page.tsx index cb6ddb3..bb91a4b 100644 --- a/payment-iq/app/page.tsx +++ b/payment-iq/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { DashboardHomePage } from "./features/pages/DashboardHomePage/DashboardHomePage"; +import { DashboardHomePage } from "./features/Pages/DashboardHomePage/DashboardHomePage"; const DashboardPage = () => { return ; diff --git a/payment-iq/middleware.ts b/payment-iq/middleware.ts new file mode 100644 index 0000000..99ed90d --- /dev/null +++ b/payment-iq/middleware.ts @@ -0,0 +1,30 @@ +// middleware.ts +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; + +export function middleware(request: NextRequest) { + const token = request.cookies.get("auth_token")?.value; // Get token from cookie + + // Define protected paths + const protectedPaths = ["/dashboard", "/settings", "/admin"]; + const isProtected = protectedPaths.some((path) => + request.nextUrl.pathname.startsWith(path) + ); + + // If accessing a protected path and no token + if (isProtected && !token) { + // Redirect to login page + const loginUrl = new URL("/login", request.url); + // Optional: Add a redirect query param to return to original page after login + loginUrl.searchParams.set("redirect", request.nextUrl.pathname); + return NextResponse.redirect(loginUrl); + } + + // Allow the request to proceed if not protected or token exists + return NextResponse.next(); +} + +// Configure matcher to run middleware on specific paths +export const config = { + matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*"], // Apply to dashboard and its sub-paths +};