4 Commits

Author SHA1 Message Date
fc932dbc7e fix(security): resolve audit findings — logging, endpoints, contact details
- Stop logging submitted password forms to the browser console
- Drive analytics from NEXT_PUBLIC_UMAMI_* instead of a hardcoded vendor
  script URL and site ID; renders nothing when unset
- Replace the wildcard image remote host "**" with an explicit allowlist
  (adds DigitalOcean Spaces)
- Fix the socket URL fallback to the API port (:3001, was :4000)
- Replace placeholder and personal contact emails with support@re-quest.com,
  including the privacy policy and terms pages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:30:46 +05:30
9c0e7457e5 Merge pull request 'Added home page with dashboard content and SEO metadata' (#2) from fix/profile-contact-improvements into main
Reviewed-on: #2
2026-06-23 11:44:37 +00:00
pradeepkumar
a07484af7e refactor: decouple /home from cross-route page/layout imports
Move /home into the (user) route group so it inherits UserLayout via the
router instead of importing (user)/layout.tsx manually, and render shared
HomeDashboard content in both /home and /user/dashboard instead of importing
the dashboard route's default page export. Removes the fragile cross-route
coupling while keeping the same URL, SEO metadata, and rendered output.
2026-06-23 17:13:55 +05:30
Chinraj P
987ca11a4d Added home page with dashboard content and SEO metadata 2026-06-23 14:14:08 +05:30
17 changed files with 185 additions and 129 deletions

View File

@@ -7,18 +7,11 @@ const nextConfig: NextConfig = {
// Image optimization
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
{
protocol: "http",
hostname: "localhost",
},
{
protocol: "http",
hostname: "127.0.0.1",
},
{ protocol: "https", hostname: "*.contabostorage.com" },
{ protocol: "https", hostname: "*.amazonaws.com" },
{ protocol: "https", hostname: "*.digitaloceanspaces.com" },
{ protocol: "http", hostname: "localhost" },
{ protocol: "http", hostname: "127.0.0.1" },
],
// Don't proxy external images through Next.js server
// Avoids SSL cert issues with Contabo S3 (sin1.contabostorage.com)

View File

@@ -3,8 +3,8 @@
import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings';
export default function PasswordSecurityPage() {
const handleSave = (data: { currentPassword: string; newPassword: string }) => {
console.log('Updating agent password:', data);
const handleSave = (_data: { currentPassword: string; newPassword: string }) => {
// TODO: call the change-password API. Do not log password payloads.
};
return (

View File

@@ -0,0 +1,11 @@
import { HomeDashboard } from '@/components/home/HomeDashboard';
export const metadata = {
title: 'RE-Quest - Connect with Trusted Real Estate Professionals',
description:
'Discover verified real estate professionals for buying, selling, renting, and investing. Search by location, specialization, and expertise to connect with trusted agents on RE-Quest.',
};
export default function HomePage() {
return <HomeDashboard />;
}

View File

@@ -1,23 +1,26 @@
'use client';
"use client";
import { useSession } from 'next-auth/react';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
import Image from 'next/image';
import { Footer } from '@/components/layout/Footer';
import { CommonHeader } from '@/components/layout/CommonHeader';
import { PresenceProvider } from '@/components/providers/presence-provider';
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
import Image from "next/image";
import { Footer } from "@/components/layout/Footer";
import { CommonHeader } from "@/components/layout/CommonHeader";
import { PresenceProvider } from "@/components/providers/presence-provider";
// Pages that don't require authentication
const publicPaths = [
'/user/dashboard',
'/user/profiles',
'/user/profile/', // Agent profile view (includes /user/profile/[id])
"/home",
"/user/dashboard",
"/user/profiles",
"/user/profile/", // Agent profile view (includes /user/profile/[id])
];
// Check if current path is public
const isPublicPath = (pathname: string) => {
return publicPaths.some(path => pathname === path || pathname.startsWith(path));
return publicPaths.some(
(path) => pathname === path || pathname.startsWith(path),
);
};
export default function UserLayout({
@@ -28,11 +31,12 @@ export default function UserLayout({
const { data: session, status } = useSession();
const router = useRouter();
const pathname = usePathname();
const isDashboard = pathname === '/user/dashboard';
// const isDashboard = pathname === "/user/dashboard";
const isDashboard = pathname === "/user/dashboard" || pathname === "/home";
const isPublic = isPublicPath(pathname);
useEffect(() => {
if (status === 'loading') return;
if (status === "loading") return;
// Allow public pages without authentication.
// Agents are intentionally allowed to land on /user/dashboard — the logo
@@ -49,19 +53,31 @@ export default function UserLayout({
// Redirect agents to agent dashboard for protected user pages
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
if (userRole === "AGENT") {
router.replace("/agent/dashboard");
}
}, [session, status, router, pathname, isPublic]);
const splashLoading = (
<div
className="min-h-screen flex flex-col items-center justify-center"
style={{ background: 'linear-gradient(to bottom, #c4d9d4, #f0f5fc)' }}
style={{ background: "linear-gradient(to bottom, #c4d9d4, #f0f5fc)" }}
>
<Image src="/assets/images/splash-house.png" alt="" width={150} height={108} priority />
<Image
src="/assets/images/splash-house.png"
alt=""
width={150}
height={108}
priority
/>
<div className="mt-[35px]">
<Image src="/assets/images/splash-logo.png" alt="RE-Quest" width={264} height={55} priority />
<Image
src="/assets/images/splash-logo.png"
alt="RE-Quest"
width={264}
height={55}
priority
/>
</div>
<div className="mt-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]" />
@@ -70,7 +86,7 @@ export default function UserLayout({
);
// Show loading only for protected pages while checking auth
if (status === 'loading' && !isPublic) {
if (status === "loading" && !isPublic) {
return splashLoading;
}
@@ -105,9 +121,7 @@ export default function UserLayout({
</div>
{/* Main Content */}
<main className="flex-1">
{children}
</main>
<main className="flex-1">{children}</main>
</>
)}

View File

@@ -1,53 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { HeroSection } from '@/components/home/HeroSection';
import { FeaturesSection } from '@/components/home/FeaturesSection';
import { TopProfessionals } from '@/components/home/TopProfessionals';
import { TestimonialsSection } from '@/components/home/TestimonialsSection';
import { cmsService, resolveImageUrl } from '@/services/cms.service';
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent, CmsContentRecord } from '@/types/cms';
import { HomeDashboard } from '@/components/home/HomeDashboard';
export default function UserDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({});
const [cmsLoaded, setCmsLoaded] = useState(false);
useEffect(() => {
const fetchCms = async () => {
try {
const sections = await cmsService.getPageContent('landing');
const data: Record<string, unknown> = {};
for (const s of sections) {
const content = s.content as Record<string, unknown>;
// Resolve S3 keys in image fields
if (s.sectionKey === 'features' && Array.isArray(content.features)) {
for (const feat of content.features as { iconPath?: string }[]) {
if (feat.iconPath) feat.iconPath = await resolveImageUrl(feat.iconPath);
}
}
if (s.sectionKey === 'testimonials' && Array.isArray(content.stats)) {
for (const stat of content.stats as { iconPath?: string }[]) {
if (stat.iconPath) stat.iconPath = await resolveImageUrl(stat.iconPath);
}
}
data[s.sectionKey] = content;
}
setCmsData(data);
} catch {
// Use default content on error
} finally {
setCmsLoaded(true);
}
};
fetchCms();
}, []);
return (
<div>
<HeroSection content={cmsData.hero as HeroContent | undefined} />
{cmsLoaded && <FeaturesSection content={cmsData.features as FeaturesContent | undefined} />}
<TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
<TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
</div>
);
return <HomeDashboard />;
}

View File

@@ -3,8 +3,8 @@
import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings';
export default function UserPasswordSecurityPage() {
const handleSave = (data: { currentPassword: string; newPassword: string }) => {
console.log('Updating user password:', data);
const handleSave = (_data: { currentPassword: string; newPassword: string }) => {
// TODO: call the change-password API. Do not log password payloads.
};
return (

View File

@@ -31,7 +31,7 @@ interface ContactCta {
const defaultContactDetails: ContactDetails = {
title: 'Get In Touch',
description: 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.',
email: '123support@gmail.com',
email: 'support@re-quest.com',
phone: '1234567890',
phoneHours: 'Mon-Fri 9am-6pm',
officeAddress: '123 Market Street',

View File

@@ -262,7 +262,7 @@ export default function FAQPage() {
Start Live Chat
</Link>
<a
href="mailto:support@requesn.com"
href="mailto:support@re-quest.com"
className="flex items-center justify-center gap-2 w-[174px] h-[51px] border border-[#00293d] rounded-[7px] font-fractul text-[16px] text-[#00293d] hover:bg-gray-50 transition-colors"
>
<Image

View File

@@ -158,12 +158,15 @@ export default function RootLayout({
<NotificationProvider />
{children}
</SessionProvider>
{/* Umami analytics — loaded after page becomes interactive */}
<Script
src="https://analytics.superlabs.co/script.js"
data-website-id="00e1ce31-e174-4519-8b59-63e8d4556b01"
strategy="afterInteractive"
/>
{/* Umami analytics — set NEXT_PUBLIC_UMAMI_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID to enable */}
{process.env.NEXT_PUBLIC_UMAMI_URL &&
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
<Script
src={process.env.NEXT_PUBLIC_UMAMI_URL}
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
strategy="afterInteractive"
/>
)}
{/* Microsoft Clarity */}
{process.env.NEXT_PUBLIC_CLARITY_ID && (
<Script id="ms-clarity" strategy="afterInteractive">

View File

@@ -1,27 +1,27 @@
'use client';
"use client";
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'loading') return;
if (status === "loading") return;
// Redirect based on user role, or to public dashboard if not logged in
if (session) {
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
if (userRole === "AGENT") {
router.replace("/agent/dashboard");
} else {
router.replace('/user/dashboard');
router.replace("/user/dashboard");
}
} else {
// Not logged in - go to public user dashboard
router.replace('/user/dashboard');
router.replace("/home");
}
}, [session, status, router]);

View File

@@ -477,10 +477,10 @@ export default function PrivacyPolicyPage() {
<p className="mb-1">
Email:{' '}
<a
href="mailto:request.sha@gmail.com"
href="mailto:support@re-quest.com"
className="text-[#e58625] underline hover:opacity-80"
>
request.sha@gmail.com
support@re-quest.com
</a>
</p>
<p>Address: 1975 Peralta Point, Colorado Springs, CO 80910</p>

View File

@@ -163,7 +163,7 @@ export default function TermsOfServicePage() {
<p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-4">
If you experience any threatening, abusive, or suspicious behavior from another user, please report the
interaction immediately using the in-app reporting feature or by contacting us at
officialteam.request@gmail.com. RE-Quest will investigate reported incidents and take appropriate action,
support@re-quest.com. RE-Quest will investigate reported incidents and take appropriate action,
which may include account suspension or referral to law enforcement.
</p>
@@ -464,7 +464,7 @@ export default function TermsOfServicePage() {
If you have any questions or concerns about these Terms, please contact us:
</p>
<p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-1">
<span className="font-bold">Email:</span> officialteam.request@gmail.com
<span className="font-bold">Email:</span> support@re-quest.com
</p>
<p className="font-serif text-[15px] leading-[24px] text-[#00293d]">
<span className="font-bold">Address:</span> 1975 Peralta Point, Colorado Springs, CO 80910

View File

@@ -0,0 +1,54 @@
'use client';
import { useState, useEffect } from 'react';
import { HeroSection } from '@/components/home/HeroSection';
import { FeaturesSection } from '@/components/home/FeaturesSection';
import { TopProfessionals } from '@/components/home/TopProfessionals';
import { TestimonialsSection } from '@/components/home/TestimonialsSection';
import { cmsService, resolveImageUrl } from '@/services/cms.service';
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent } from '@/types/cms';
// Shared landing/dashboard content rendered by both /home and /user/dashboard.
export function HomeDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({});
const [cmsLoaded, setCmsLoaded] = useState(false);
useEffect(() => {
const fetchCms = async () => {
try {
const sections = await cmsService.getPageContent('landing');
const data: Record<string, unknown> = {};
for (const s of sections) {
const content = s.content as Record<string, unknown>;
// Resolve S3 keys in image fields
if (s.sectionKey === 'features' && Array.isArray(content.features)) {
for (const feat of content.features as { iconPath?: string }[]) {
if (feat.iconPath) feat.iconPath = await resolveImageUrl(feat.iconPath);
}
}
if (s.sectionKey === 'testimonials' && Array.isArray(content.stats)) {
for (const stat of content.stats as { iconPath?: string }[]) {
if (stat.iconPath) stat.iconPath = await resolveImageUrl(stat.iconPath);
}
}
data[s.sectionKey] = content;
}
setCmsData(data);
} catch {
// Use default content on error
} finally {
setCmsLoaded(true);
}
};
fetchCms();
}, []);
return (
<div>
<HeroSection content={cmsData.hero as HeroContent | undefined} />
{cmsLoaded && <FeaturesSection content={cmsData.features as FeaturesContent | undefined} />}
<TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
<TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
</div>
);
}

View File

@@ -7,6 +7,7 @@ import { useSession } from "next-auth/react";
import { useHeaderData } from "@/components/providers/header-provider";
const navLinks = [
{ label: "Professional", href: "/user/profiles" },
{ label: "Education", href: "/education" },
{ label: "About Us", href: "/about" },
{ label: "FAQ's", href: "/faq" },
@@ -56,6 +57,7 @@ export function CommonHeader() {
if (showProfileMenu || showGuestMenu || showMobileMenu) {
document.addEventListener("mousedown", handleClickOutside);
}
// const showProfessional = userRole === "AGENT" || userRole === "LENDER";
return () => {
document.removeEventListener("mousedown", handleClickOutside);
@@ -66,12 +68,13 @@ export function CommonHeader() {
const userName = profileName || session?.user?.name;
const userEmail = session?.user?.email;
const userRole = (session?.user as any)?.role;
const showProfessional = userRole === "AGENT" || userRole === "LENDER";
// const showProfessional = userRole === "AGENT" || userRole === "LENDER";
// Use fetched profile image, fallback to session image
const userImage = profileImage || session?.user?.image;
// Logo destination — always lands on the user dashboard regardless of role.
const dashboardLink = "/user/dashboard";
// const dashboardLink = "/user/dashboard";
const dashboardLink = "/home";
return (
<header
@@ -92,7 +95,7 @@ export function CommonHeader() {
</Link>
{/* Navigation - Desktop only */}
{/* <nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
{navLinks.map((link) => (
<Link
key={link.href}
@@ -102,8 +105,8 @@ export function CommonHeader() {
{link.label}
</Link>
))}
</nav> */}
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
</nav>
{/* <nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
{showProfessional && (
<Link
href="/user/profiles"
@@ -122,7 +125,7 @@ export function CommonHeader() {
{link.label}
</Link>
))}
</nav>
</nav> */}
{/* Right Side Icons */}
<div className="flex items-center gap-2 md:gap-4">
@@ -470,7 +473,7 @@ export function CommonHeader() {
{/* Mobile Navigation Menu */}
{showMobileMenu && (
<div className="md:hidden border-t border-white/20 py-3 pb-4">
{/* <nav className="flex flex-col gap-1">
<nav className="flex flex-col gap-1">
{navLinks.map((link) => (
<Link
key={link.href}
@@ -481,8 +484,8 @@ export function CommonHeader() {
{link.label}
</Link>
))}
</nav> */}
<nav className="flex flex-col gap-1">
</nav>
{/* <nav className="flex flex-col gap-1">
{showProfessional && (
<Link
href="/user/profiles"
@@ -503,7 +506,7 @@ export function CommonHeader() {
{link.label}
</Link>
))}
</nav>
</nav> */}
</div>
)}
</header>

View File

@@ -270,10 +270,10 @@ export function SubscriptionForm() {
{/* Support Email */}
<Link
href="mailto:support@example.com"
href="mailto:support@re-quest.com"
className="font-serif font-bold text-[14px] text-[#e58625] underline hover:text-[#d47920] transition-colors"
>
support@example.com
support@re-quest.com
</Link>
</div>

View File

@@ -2,10 +2,30 @@ import { auth } from "@/auth";
import { NextResponse } from "next/server";
// Auth routes - logged-in users should be redirected away from these
const authRoutes = ["/login", "/signup", "/forgot-password", "/reset-password", "/verify-email"];
const authRoutes = [
"/login",
"/signup",
"/forgot-password",
"/reset-password",
"/verify-email",
];
// Public routes - accessible to everyone (logged in or not)
const publicRoutes = ["/", "/contact", "/about", "/faq", "/education", "/coming-soon", "/privacy-policy", "/terms-of-service", "/logout", "/user/dashboard", "/user/profiles", "/user/profile"];
const publicRoutes = [
"/",
"/home",
"/contact",
"/about",
"/faq",
"/education",
"/coming-soon",
"/privacy-policy",
"/terms-of-service",
"/logout",
"/user/dashboard",
"/user/profiles",
"/user/profile",
];
// Routes that should NEVER be redirected away from (even if logged in)
const noRedirectRoutes = ["/logout"];
@@ -16,23 +36,27 @@ export default auth((req) => {
const userRole = (req.auth?.user as any)?.role;
const isAuthRoute = authRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
const isPublicRoute = publicRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
const isAgentRoute = nextUrl.pathname.startsWith("/agent");
const isUserRoute = nextUrl.pathname.startsWith("/user");
const isApiRoute = nextUrl.pathname.startsWith("/api");
const isStaticRoute = nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
const isStaticRoute =
nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
const isNoRedirectRoute = noRedirectRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
// Skip middleware for API routes and static files

View File

@@ -59,7 +59,7 @@ class SocketService {
}
// Extract base URL without /api/v1 path for Socket.io connection
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, '');
this.socket = io(baseUrl, {