feat: Implement authentication checks for profile actions and user layout, and add presigned URL fetching for file viewing.

This commit is contained in:
pradeepkumar
2026-01-29 00:02:59 +05:30
parent 4ffd6305b1
commit b41bd8a3eb
9 changed files with 243 additions and 40 deletions

View File

@@ -444,6 +444,7 @@ function RangeSlider({ field, value, onChange }: RangeSliderProps) {
// File Upload Types
interface UploadedFile {
id: string;
key?: string; // S3 key for fetching presigned URL
name: string;
size: number;
type: string;
@@ -459,6 +460,12 @@ interface UploadingFile {
error?: string;
}
// Map to store presigned URLs by file ID
interface FileWithPresignedUrl extends UploadedFile {
presignedUrl?: string;
isLoadingUrl?: boolean;
}
// File Upload Component
interface FileUploadProps {
field: ProfileField;
@@ -471,6 +478,8 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState(false);
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
const allowedFormats = field.validation?.allowedFormats || ['PDF', 'DOCX', 'JPG', 'PNG'];
@@ -631,6 +640,42 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
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;
return (
@@ -744,20 +789,14 @@ function FileUpload({ field, fieldSlug, value, onChange }: FileUploadProps) {
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
{file.url ? (
<a
href={file.url}
target="_blank"
rel="noopener noreferrer"
className="text-[14px] font-serif text-[#E58625] hover:underline truncate"
>
{file.name}
</a>
) : (
<span className="text-[14px] font-serif text-[#00293D] truncate">
{file.name}
</span>
)}
<button
type="button"
onClick={() => handleViewFile(file)}
disabled={loadingUrls[file.id]}
className="text-[14px] font-serif text-[#E58625] hover:underline truncate text-left disabled:opacity-50"
>
{loadingUrls[file.id] ? 'Loading...' : file.name}
</button>
<button
type="button"
onClick={() => handleRemoveFile(file.id)}

View File

@@ -1,9 +1,9 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { signIn } from 'next-auth/react';
import Link from 'next/link';
import { authService, AuthService, RegisterRequest } from '@/services';
import { authService, AuthService, RegisterRequest, agentsService, AgentType } from '@/services';
type UserType = 'USER' | 'AGENT';
@@ -23,6 +23,25 @@ export default function SignUpPage() {
const [fieldErrors, setFieldErrors] = useState<ValidationError[]>([]);
const [loading, setLoading] = 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 fieldError = fieldErrors.find((e) => e.field === fieldName);
@@ -35,6 +54,13 @@ export default function SignUpPage() {
setError('');
setFieldErrors([]);
// Validate agent type if registering as agent
if (userType === 'AGENT' && !selectedAgentTypeId) {
setError('Please select an agent type');
setLoading(false);
return;
}
try {
const registerData: RegisterRequest = {
firstName,
@@ -42,6 +68,7 @@ export default function SignUpPage() {
email,
password,
role: userType,
...(userType === 'AGENT' && selectedAgentTypeId && { agentTypeId: selectedAgentTypeId }),
};
await authService.register(registerData);
@@ -228,6 +255,37 @@ export default function SignUpPage() {
</p>
</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 */}
<button
type="submit"

View File

@@ -6,6 +6,18 @@ import { useEffect } from 'react';
import { Footer } from '@/components/layout/Footer';
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({
children,
}: {
@@ -16,23 +28,47 @@ export default function UserLayout({
const pathname = usePathname();
const isDashboard = pathname === '/user/dashboard';
const isPublic = isPublicPath(pathname);
useEffect(() => {
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) {
router.replace('/login');
return;
}
// Redirect agents to agent dashboard
// Redirect agents to agent dashboard for protected user pages
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
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 (
<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>

View File

@@ -11,17 +11,16 @@ export default function Home() {
useEffect(() => {
if (status === 'loading') return;
if (!session) {
router.replace('/login');
return;
}
// Redirect based on user role
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
// 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');
} else {
router.replace('/user/dashboard');
}
} else {
// Not logged in - go to public user dashboard
router.replace('/user/dashboard');
}
}, [session, status, router]);