feat: Implement authentication checks for profile actions and user layout, and add presigned URL fetching for file viewing.
This commit is contained in:
@@ -444,6 +444,7 @@ function RangeSlider({ field, value, onChange }: RangeSliderProps) {
|
|||||||
// File Upload Types
|
// File Upload Types
|
||||||
interface UploadedFile {
|
interface UploadedFile {
|
||||||
id: string;
|
id: string;
|
||||||
|
key?: string; // S3 key for fetching presigned URL
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -459,6 +460,12 @@ interface UploadingFile {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map to store presigned URLs by file ID
|
||||||
|
interface FileWithPresignedUrl extends UploadedFile {
|
||||||
|
presignedUrl?: string;
|
||||||
|
isLoadingUrl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// File Upload Component
|
// File Upload Component
|
||||||
interface FileUploadProps {
|
interface FileUploadProps {
|
||||||
field: ProfileField;
|
field: ProfileField;
|
||||||
@@ -471,6 +478,8 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
||||||
|
const [presignedUrls, setPresignedUrls] = useState<Record<string, string>>({});
|
||||||
|
const [loadingUrls, setLoadingUrls] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// Get allowed formats from field config or use defaults
|
// Get allowed formats from field config or use defaults
|
||||||
const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG'];
|
const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG'];
|
||||||
@@ -631,6 +640,42 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewFile = async (file: UploadedFile) => {
|
||||||
|
// Check if we already have a presigned URL
|
||||||
|
if (presignedUrls[file.id]) {
|
||||||
|
window.open(presignedUrls[file.id], '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the key - either from file.key, file.id, or file.url if it's an S3 key
|
||||||
|
const fileKey = file.key || file.id || file.url;
|
||||||
|
|
||||||
|
// If the URL is already a full URL (http/https), just open it directly
|
||||||
|
if (fileKey.startsWith('http://') || fileKey.startsWith('https://')) {
|
||||||
|
window.open(fileKey, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set loading state
|
||||||
|
setLoadingUrls(prev => ({ ...prev, [file.id]: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { uploadService } = await import('@/services/upload.service');
|
||||||
|
const presignedUrl = await uploadService.getPresignedDownloadUrl(fileKey);
|
||||||
|
|
||||||
|
// Store for future use
|
||||||
|
setPresignedUrls(prev => ({ ...prev, [file.id]: presignedUrl }));
|
||||||
|
|
||||||
|
// Open the file
|
||||||
|
window.open(presignedUrl, '_blank');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get presigned URL:', error);
|
||||||
|
alert('Failed to open file. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setLoadingUrls(prev => ({ ...prev, [file.id]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isUploading = uploadingFiles.length > 0;
|
const isUploading = uploadingFiles.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -744,20 +789,14 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
|
|||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{file.url ? (
|
<button
|
||||||
<a
|
type="button"
|
||||||
href={file.url}
|
onClick={() => handleViewFile(file)}
|
||||||
target="_blank"
|
disabled={loadingUrls[file.id]}
|
||||||
rel="noopener noreferrer"
|
className="text-[14px] font-serif text-[#E58625] hover:underline truncate text-left disabled:opacity-50"
|
||||||
className="text-[14px] font-serif text-[#E58625] hover:underline truncate"
|
>
|
||||||
>
|
{loadingUrls[file.id] ? 'Loading...' : file.name}
|
||||||
{file.name}
|
</button>
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="text-[14px] font-serif text-[#00293D] truncate">
|
|
||||||
{file.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveFile(file.id)}
|
onClick={() => handleRemoveFile(file.id)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { authService, AuthService, RegisterRequest } from '@/services';
|
import { authService, AuthService, RegisterRequest, agentsService, AgentType } from '@/services';
|
||||||
|
|
||||||
type UserType = 'USER' | 'AGENT';
|
type UserType = 'USER' | 'AGENT';
|
||||||
|
|
||||||
@@ -23,6 +23,25 @@ export default function SignUpPage() {
|
|||||||
const [fieldErrors, setFieldErrors] = useState<ValidationError[]>([]);
|
const [fieldErrors, setFieldErrors] = useState<ValidationError[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [registrationSuccess, setRegistrationSuccess] = useState(false);
|
const [registrationSuccess, setRegistrationSuccess] = useState(false);
|
||||||
|
const [agentTypes, setAgentTypes] = useState<AgentType[]>([]);
|
||||||
|
const [selectedAgentTypeId, setSelectedAgentTypeId] = useState('');
|
||||||
|
const [loadingAgentTypes, setLoadingAgentTypes] = useState(false);
|
||||||
|
|
||||||
|
// Fetch agent types on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAgentTypes = async () => {
|
||||||
|
setLoadingAgentTypes(true);
|
||||||
|
try {
|
||||||
|
const types = await agentsService.getAgentTypes();
|
||||||
|
setAgentTypes(types);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch agent types:', err);
|
||||||
|
} finally {
|
||||||
|
setLoadingAgentTypes(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchAgentTypes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const getFieldError = (fieldName: string): string | null => {
|
const getFieldError = (fieldName: string): string | null => {
|
||||||
const fieldError = fieldErrors.find((e) => e.field === fieldName);
|
const fieldError = fieldErrors.find((e) => e.field === fieldName);
|
||||||
@@ -35,6 +54,13 @@ export default function SignUpPage() {
|
|||||||
setError('');
|
setError('');
|
||||||
setFieldErrors([]);
|
setFieldErrors([]);
|
||||||
|
|
||||||
|
// Validate agent type if registering as agent
|
||||||
|
if (userType === 'AGENT' && !selectedAgentTypeId) {
|
||||||
|
setError('Please select an agent type');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const registerData: RegisterRequest = {
|
const registerData: RegisterRequest = {
|
||||||
firstName,
|
firstName,
|
||||||
@@ -42,6 +68,7 @@ export default function SignUpPage() {
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role: userType,
|
role: userType,
|
||||||
|
...(userType === 'AGENT' && selectedAgentTypeId && { agentTypeId: selectedAgentTypeId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
await authService.register(registerData);
|
await authService.register(registerData);
|
||||||
@@ -228,6 +255,37 @@ export default function SignUpPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Agent Type Selection - Only for Agents */}
|
||||||
|
{userType === 'AGENT' && (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={selectedAgentTypeId}
|
||||||
|
onChange={(e) => setSelectedAgentTypeId(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loadingAgentTypes}
|
||||||
|
className={`w-full px-6 py-6 bg-[#f0f5fc] rounded-[20px] text-[#00293d] text-sm font-light focus:outline-none focus:ring-2 focus:ring-[#00293d]/20 transition-all appearance-none cursor-pointer ${
|
||||||
|
!selectedAgentTypeId ? 'text-[#00293d]/60' : ''
|
||||||
|
} ${getFieldError('agentTypeId') ? 'ring-2 ring-red-400' : ''}`}
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2300293d'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E")`,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundPosition: 'right 1.5rem center',
|
||||||
|
backgroundSize: '1.25rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Select Agent Type</option>
|
||||||
|
{agentTypes.map((type) => (
|
||||||
|
<option key={type.id} value={type.id}>
|
||||||
|
{type.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{getFieldError('agentTypeId') && (
|
||||||
|
<p className="text-red-500 text-xs mt-1 ml-2">{getFieldError('agentTypeId')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Sign Up Button */}
|
{/* Sign Up Button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ import { useEffect } from 'react';
|
|||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { CommonHeader } from '@/components/layout/CommonHeader';
|
import { CommonHeader } from '@/components/layout/CommonHeader';
|
||||||
|
|
||||||
|
// Pages that don't require authentication
|
||||||
|
const publicPaths = [
|
||||||
|
'/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));
|
||||||
|
};
|
||||||
|
|
||||||
export default function UserLayout({
|
export default function UserLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -16,23 +28,47 @@ export default function UserLayout({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const isDashboard = pathname === '/user/dashboard';
|
const isDashboard = pathname === '/user/dashboard';
|
||||||
|
const isPublic = isPublicPath(pathname);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'loading') return;
|
if (status === 'loading') return;
|
||||||
|
|
||||||
|
// Allow public pages without authentication
|
||||||
|
if (isPublic) {
|
||||||
|
// If logged in as agent, redirect to agent dashboard only from user dashboard
|
||||||
|
if (session && pathname === '/user/dashboard') {
|
||||||
|
const userRole = (session.user as any)?.role;
|
||||||
|
if (userRole === 'AGENT') {
|
||||||
|
router.replace('/agent/dashboard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protected pages require login
|
||||||
if (!session) {
|
if (!session) {
|
||||||
router.replace('/login');
|
router.replace('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect agents to agent dashboard
|
// Redirect agents to agent dashboard for protected user pages
|
||||||
const userRole = (session.user as any)?.role;
|
const userRole = (session.user as any)?.role;
|
||||||
if (userRole === 'AGENT') {
|
if (userRole === 'AGENT') {
|
||||||
router.replace('/agent/dashboard');
|
router.replace('/agent/dashboard');
|
||||||
}
|
}
|
||||||
}, [session, status, router]);
|
}, [session, status, router, pathname, isPublic]);
|
||||||
|
|
||||||
if (status === 'loading' || !session) {
|
// Show loading only for protected pages while checking auth
|
||||||
|
if (status === 'loading' && !isPublic) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect protected pages if not logged in
|
||||||
|
if (!session && !isPublic) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]"></div>
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ export default function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'loading') return;
|
if (status === 'loading') return;
|
||||||
|
|
||||||
if (!session) {
|
// Redirect based on user role, or to public dashboard if not logged in
|
||||||
router.replace('/login');
|
if (session) {
|
||||||
return;
|
const userRole = (session.user as any)?.role;
|
||||||
}
|
if (userRole === 'AGENT') {
|
||||||
|
router.replace('/agent/dashboard');
|
||||||
// Redirect based on user role
|
} else {
|
||||||
const userRole = (session.user as any)?.role;
|
router.replace('/user/dashboard');
|
||||||
|
}
|
||||||
if (userRole === 'AGENT') {
|
|
||||||
router.replace('/agent/dashboard');
|
|
||||||
} else {
|
} else {
|
||||||
|
// Not logged in - go to public user dashboard
|
||||||
router.replace('/user/dashboard');
|
router.replace('/user/dashboard');
|
||||||
}
|
}
|
||||||
}, [session, status, router]);
|
}, [session, status, router]);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { Tag } from './Tag';
|
import { Tag } from './Tag';
|
||||||
|
|
||||||
interface ProfileCardProps {
|
interface ProfileCardProps {
|
||||||
@@ -33,6 +35,26 @@ export function ProfileCard({
|
|||||||
messageHref,
|
messageHref,
|
||||||
requestsHref,
|
requestsHref,
|
||||||
}: ProfileCardProps) {
|
}: ProfileCardProps) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Handle action click - require login if not authenticated
|
||||||
|
const handleActionClick = (e: React.MouseEvent, targetHref?: string) => {
|
||||||
|
if (!session) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Store the current page to redirect back after login
|
||||||
|
const returnUrl = encodeURIComponent(pathname);
|
||||||
|
router.push(`/login?callbackUrl=${returnUrl}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If logged in and has href, navigation will happen naturally via Link
|
||||||
|
if (!targetHref) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Handle button click when logged in but no href specified
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
<div className="bg-white rounded-[20px] p-4 lg:p-5">
|
||||||
{/* Name and Actions Row */}
|
{/* Name and Actions Row */}
|
||||||
@@ -99,6 +121,7 @@ export function ProfileCard({
|
|||||||
{messageHref ? (
|
{messageHref ? (
|
||||||
<Link
|
<Link
|
||||||
href={messageHref}
|
href={messageHref}
|
||||||
|
onClick={(e) => handleActionClick(e, messageHref)}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -111,6 +134,7 @@ export function ProfileCard({
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
onClick={(e) => handleActionClick(e)}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -125,6 +149,7 @@ export function ProfileCard({
|
|||||||
{requestsHref ? (
|
{requestsHref ? (
|
||||||
<Link
|
<Link
|
||||||
href={requestsHref}
|
href={requestsHref}
|
||||||
|
onClick={(e) => handleActionClick(e, requestsHref)}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -137,6 +162,7 @@ export function ProfileCard({
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
|
onClick={(e) => handleActionClick(e)}
|
||||||
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
className="flex items-center gap-2 px-4 py-1.5 bg-[#e58625] text-[#00293d] text-sm font-normal rounded-[15px] border border-[#00293d]/10 hover:bg-[#d47720] transition-colors font-serif cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
|
|
||||||
interface StatusButtonsProps {
|
interface StatusButtonsProps {
|
||||||
isAvailable?: boolean;
|
isAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusButtons({ isAvailable = true }: StatusButtonsProps) {
|
export function StatusButtons({ isAvailable = true }: StatusButtonsProps) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Handle connect click - require login if not authenticated
|
||||||
|
const handleConnectClick = () => {
|
||||||
|
if (!session) {
|
||||||
|
// Store the current page to redirect back after login
|
||||||
|
const returnUrl = encodeURIComponent(pathname);
|
||||||
|
router.push(`/login?callbackUrl=${returnUrl}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: Handle connect action when logged in
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-[354px] lg:max-w-none space-y-2">
|
<div className="w-full max-w-[354px] lg:max-w-none space-y-2">
|
||||||
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
<div className="flex items-center border border-[#00293d]/10 rounded-[15px] h-[50px] px-4">
|
||||||
@@ -12,7 +30,10 @@ export function StatusButtons({ isAvailable = true }: StatusButtonsProps) {
|
|||||||
<span className="w-3 h-3 bg-green-500 rounded-full" />
|
<span className="w-3 h-3 bg-green-500 rounded-full" />
|
||||||
<span className="text-sm font-bold text-[#00293d] font-fractul">Available.</span>
|
<span className="text-sm font-bold text-[#00293d] font-fractul">Available.</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="px-5 py-1.5 bg-[#e58625] text-white text-sm rounded-[15px] hover:bg-[#d47720] transition-colors font-fractul">
|
<button
|
||||||
|
onClick={handleConnectClick}
|
||||||
|
className="px-5 py-1.5 bg-[#e58625] text-white text-sm rounded-[15px] hover:bg-[#d47720] transition-colors font-fractul"
|
||||||
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,7 +42,10 @@ export function StatusButtons({ isAvailable = true }: StatusButtonsProps) {
|
|||||||
<span className="w-3 h-3 bg-white border border-gray-400 rounded-full" />
|
<span className="w-3 h-3 bg-white border border-gray-400 rounded-full" />
|
||||||
<span className="text-sm font-bold text-[#00293d] font-fractul">Unavailable.</span>
|
<span className="text-sm font-bold text-[#00293d] font-fractul">Unavailable.</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="px-5 py-1.5 bg-[#00293d] text-white text-sm rounded-[15px] hover:bg-[#001d2b] transition-colors font-fractul">
|
<button
|
||||||
|
onClick={handleConnectClick}
|
||||||
|
className="px-5 py-1.5 bg-[#00293d] text-white text-sm rounded-[15px] hover:bg-[#001d2b] transition-colors font-fractul"
|
||||||
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { NextResponse } from "next/server";
|
|||||||
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)
|
// Public routes - accessible to everyone (logged in or not)
|
||||||
const publicRoutes = ["/", "/contact", "/about", "/faq"];
|
const publicRoutes = ["/", "/contact", "/about", "/faq", "/user/dashboard", "/user/profiles", "/user/profile"];
|
||||||
|
|
||||||
export default auth((req) => {
|
export default auth((req) => {
|
||||||
const { nextUrl } = req;
|
const { nextUrl } = req;
|
||||||
@@ -58,8 +58,8 @@ export default auth((req) => {
|
|||||||
return NextResponse.redirect(new URL("/user/dashboard", nextUrl.origin));
|
return NextResponse.redirect(new URL("/user/dashboard", nextUrl.origin));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent agents from accessing user routes
|
// Prevent agents from accessing protected user routes (but allow public user routes)
|
||||||
if (isUserRoute && userRole === "AGENT") {
|
if (isUserRoute && userRole === "AGENT" && !isPublicRoute) {
|
||||||
return NextResponse.redirect(new URL("/agent/dashboard", nextUrl.origin));
|
return NextResponse.redirect(new URL("/agent/dashboard", nextUrl.origin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,19 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Public API endpoints that don't require authentication
|
||||||
|
const publicEndpoints = [
|
||||||
|
'/agents',
|
||||||
|
'/agent-types',
|
||||||
|
'/profiles',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if URL is a public endpoint
|
||||||
|
const isPublicEndpoint = (url?: string): boolean => {
|
||||||
|
if (!url) return false;
|
||||||
|
return publicEndpoints.some(endpoint => url.includes(endpoint));
|
||||||
|
};
|
||||||
|
|
||||||
// Response interceptor with token refresh
|
// Response interceptor with token refresh
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
@@ -72,6 +85,17 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
// Handle 401 Unauthorized
|
// Handle 401 Unauthorized
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
// For public endpoints, just reject the error without redirecting to signout
|
||||||
|
// The request will work without auth anyway
|
||||||
|
if (isPublicEndpoint(originalRequest.url)) {
|
||||||
|
// Remove the auth header and retry once
|
||||||
|
if (originalRequest.headers) {
|
||||||
|
delete originalRequest.headers.Authorization;
|
||||||
|
}
|
||||||
|
originalRequest._retry = true;
|
||||||
|
return api(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't try to refresh if this was the refresh token request itself
|
// Don't try to refresh if this was the refresh token request itself
|
||||||
if (originalRequest.url?.includes('/auth/refresh')) {
|
if (originalRequest.url?.includes('/auth/refresh')) {
|
||||||
// Refresh token is also invalid, clear everything and sign out properly
|
// Refresh token is also invalid, clear everything and sign out properly
|
||||||
@@ -105,13 +129,11 @@ api.interceptors.response.use(
|
|||||||
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
// No refresh token available, sign out properly
|
// No refresh token available, just clear tokens but don't redirect for better UX
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
// Use NextAuth's signout endpoint to properly clear the session
|
|
||||||
window.location.href = '/api/auth/signout?callbackUrl=/login';
|
|
||||||
}
|
}
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@@ -145,13 +167,11 @@ api.interceptors.response.use(
|
|||||||
// Retry original request
|
// Retry original request
|
||||||
return api(originalRequest);
|
return api(originalRequest);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
// Refresh failed, clear tokens and sign out properly
|
// Refresh failed, clear tokens but don't redirect
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
// Use NextAuth's signout endpoint to properly clear the session
|
|
||||||
window.location.href = '/api/auth/signout?callbackUrl=/login';
|
|
||||||
}
|
}
|
||||||
processQueue(refreshError as AxiosError);
|
processQueue(refreshError as AxiosError);
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface RegisterRequest {
|
|||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
role: 'USER' | 'AGENT';
|
role: 'USER' | 'AGENT';
|
||||||
|
agentTypeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
|
|||||||
Reference in New Issue
Block a user