Files
cartlog-admin/SECURITY_GUIDE.md

15 KiB

Security Best Practices Guide

🔐 Authentication & Authorization

Current Issues

The application currently stores sensitive data in localStorage and has incomplete authentication checks.

1. Secure Token Storage

Current (Insecure):

// src/helper/accountContext/AccountProvider.js
localStorage.setItem("role", JSON.stringify(data?.role))

Recommended:

// Use httpOnly cookies set by the server
// Client-side code should NOT directly access tokens

// Server-side API route (example)
export async function POST(request) {
  const { email, password } = await request.json();
  
  // Authenticate user
  const user = await authenticateUser(email, password);
  
  if (user) {
    // Set httpOnly cookie
    const response = NextResponse.json({ success: true, user });
    response.cookies.set('auth_token', user.token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 * 7, // 1 week
      path: '/'
    });
    return response;
  }
  
  return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}

2. Client-Side Permission Checks

Current (Incomplete):

// src/layout/index.js
useEffect(() => {
  const securePaths = mounted && ConvertPermissionArr(data1?.permissions);
  if (mounted && !securePaths.find((item) => item?.name == replacePath(path?.split("/")[2]))) {
    router.push(`/403`);
  }
}, [data1]);

Recommended:

// Create a custom hook for permission checks
// src/utils/hooks/usePermission.js
import { useContext, useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import AccountContext from '@/helper/accountContext';

export function usePermission(requiredPermission) {
  const { accountData } = useContext(AccountContext);
  const router = useRouter();
  const pathname = usePathname();
  const [isAuthorized, setIsAuthorized] = useState(false);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (!accountData) {
      setIsLoading(true);
      return;
    }

    const userPermissions = accountData?.permissions || [];
    const hasPermission = userPermissions.some(
      (perm) => perm.name === requiredPermission
    );

    if (!hasPermission) {
      const lang = pathname.split('/')[1] || 'en';
      router.push(`/${lang}/403`);
      setIsAuthorized(false);
    } else {
      setIsAuthorized(true);
    }
    
    setIsLoading(false);
  }, [accountData, requiredPermission, router, pathname]);

  return { isAuthorized, isLoading };
}

// Usage in components
function ProductPage() {
  const { isAuthorized, isLoading } = usePermission('product.view');

  if (isLoading) {
    return <Loader />;
  }

  if (!isAuthorized) {
    return null; // Will redirect via hook
  }

  return <ProductList />;
}

🛡️ XSS Prevention

HTML Sanitization

Current (Vulnerable):

// src/utils/customFunctions/TextLimit.js
const sanitizeAndTrustHtml = (htmlString) => {
    return { __html: htmlString }; // NO SANITIZATION!
};

Recommended:

import DOMPurify from 'isomorphic-dompurify';

const sanitizeAndTrustHtml = (htmlString) => {
    // Configure DOMPurify for your needs
    const config = {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
        ALLOWED_ATTR: ['href', 'target'],
        ALLOW_DATA_ATTR: false
    };
    
    return { __html: DOMPurify.sanitize(htmlString, config) };
};

// Even better: avoid dangerouslySetInnerHTML when possible
const TextLimit = ({ value, maxLength }) => {
    if (!value) return '';

    let summarizedValue = value.substring(0, maxLength);
    if (value.length > maxLength) {
        summarizedValue += '...';
    }

    // If it's plain text, just render it
    if (!containsHtmlTags(value)) {
        return <div>{summarizedValue}</div>;
    }

    // Only use dangerouslySetInnerHTML for trusted, sanitized content
    const sanitizedValue = sanitizeAndTrustHtml(summarizedValue);
    return <div dangerouslySetInnerHTML={sanitizedValue} />;
};

User Input Validation

Always validate and sanitize user input:

// src/utils/validation/inputSanitization.js
export function sanitizeInput(input) {
    if (typeof input !== 'string') return input;
    
    // Remove potentially dangerous characters
    return input
        .replace(/[<>]/g, '') // Remove < and >
        .trim();
}

export function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

export function sanitizeFileName(filename) {
    // Remove path traversal attempts
    return filename
        .replace(/\.\./g, '')
        .replace(/[\/\\]/g, '')
        .trim();
}

// Usage in forms
function handleSubmit(values) {
    const sanitizedValues = {
        name: sanitizeInput(values.name),
        email: values.email, // Already validated by Yup
        description: sanitizeInput(values.description)
    };
    
    // Submit sanitized values
    await submitForm(sanitizedValues);
}

🔒 API Security

Request Validation

Implement proper request validation:

// src/app/api/products/route.js
import { NextResponse } from 'next/server';
import { z } from 'zod';

// Define schema for validation
const productSchema = z.object({
    name: z.string().min(1).max(255),
    price: z.number().positive(),
    description: z.string().max(5000).optional(),
    category_id: z.number().int().positive()
});

export async function POST(request) {
    try {
        // Verify authentication
        const token = request.cookies.get('auth_token')?.value;
        if (!token) {
            return NextResponse.json(
                { error: 'Unauthorized' },
                { status: 401 }
            );
        }

        // Parse and validate request body
        const body = await request.json();
        const validatedData = productSchema.parse(body);

        // Check permissions
        const user = await verifyToken(token);
        if (!user.permissions.includes('product.create')) {
            return NextResponse.json(
                { error: 'Forbidden' },
                { status: 403 }
            );
        }

        // Process request
        const product = await createProduct(validatedData);
        
        return NextResponse.json({ success: true, product });
    } catch (error) {
        if (error instanceof z.ZodError) {
            return NextResponse.json(
                { error: 'Validation failed', details: error.errors },
                { status: 400 }
            );
        }
        
        console.error('API Error:', error);
        return NextResponse.json(
            { error: 'Internal server error' },
            { status: 500 }
        );
    }
}

Rate Limiting

Implement rate limiting:

// src/middleware.js
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
    redis: Redis.fromEnv(),
    limiter: Ratelimit.slidingWindow(10, '10 s'),
});

export async function middleware(request) {
    // Rate limit API routes
    if (request.nextUrl.pathname.startsWith('/api/')) {
        const ip = request.ip ?? '127.0.0.1';
        const { success, limit, reset, remaining } = await ratelimit.limit(ip);

        if (!success) {
            return NextResponse.json(
                { error: 'Too many requests' },
                { 
                    status: 429,
                    headers: {
                        'X-RateLimit-Limit': limit.toString(),
                        'X-RateLimit-Remaining': remaining.toString(),
                        'X-RateLimit-Reset': reset.toString()
                    }
                }
            );
        }
    }

    return NextResponse.next();
}

🔐 Environment Variables

Secure Configuration

Current:

// next.config.js
env: {
    API_PROD_URL: "https://fastkart-admin-json.vercel.app/api/",
}

Recommended:

Create .env.local:

# Public variables (accessible in browser)
NEXT_PUBLIC_API_URL=http://localhost:3000/api
NEXT_PUBLIC_APP_NAME=FastKart Admin

# Private variables (server-side only)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
JWT_SECRET=your-super-secret-key-change-this
API_SECRET_KEY=another-secret-key

# Third-party services
STRIPE_SECRET_KEY=sk_test_...
SENDGRID_API_KEY=SG...

Create .env.production:

NEXT_PUBLIC_API_URL=https://api.fastkart.com
DATABASE_URL=postgresql://prod-user:prod-pass@prod-host:5432/prod-db
JWT_SECRET=production-secret-key

Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
    reactStrictMode: true,
    
    // Remove hardcoded env variables
    // They will be loaded from .env files automatically
    
    images: {
        remotePatterns: [
            {
                protocol: 'https',
                hostname: 'fastkart-admin-json.vercel.app',
            },
        ],
    },
};

module.exports = nextConfig;

⚠️ Important: Add to .gitignore:

.env
.env*.local
.env.production

🛠️ Error Handling

Centralized Error Handler

Create a centralized error handler:

// src/utils/errorHandler.js
import { toast } from 'react-toastify';

export class AppError extends Error {
    constructor(message, statusCode = 500, isOperational = true) {
        super(message);
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        Error.captureStackTrace(this, this.constructor);
    }
}

export function handleApiError(error) {
    // Log error for debugging (use proper logging service in production)
    if (process.env.NODE_ENV === 'development') {
        console.error('API Error:', error);
    }

    // Send to error tracking service (Sentry, etc.)
    if (process.env.NODE_ENV === 'production') {
        // Sentry.captureException(error);
    }

    // User-friendly error messages
    const status = error?.response?.status;
    const message = error?.response?.data?.message;

    switch (status) {
        case 400:
            toast.error(message || 'Invalid request. Please check your input.');
            break;
        case 401:
            toast.error('Your session has expired. Please login again.');
            // Redirect to login
            window.location.href = '/en/auth/login';
            break;
        case 403:
            toast.error('You do not have permission to perform this action.');
            break;
        case 404:
            toast.error('The requested resource was not found.');
            break;
        case 422:
            toast.error(message || 'Validation error. Please check your input.');
            break;
        case 429:
            toast.error('Too many requests. Please try again later.');
            break;
        case 500:
        case 502:
        case 503:
            toast.error('Server error. Please try again later.');
            break;
        default:
            toast.error(message || 'An unexpected error occurred.');
    }

    return Promise.reject(error);
}

// Usage in axios interceptor
// src/utils/axiosUtils/index.js
import { handleApiError } from '../errorHandler';

const client = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_URL,
    headers: {
        Accept: "application/json",
    },
});

client.interceptors.response.use(
    (response) => response,
    (error) => handleApiError(error)
);

📝 Logging

Structured Logging

Implement proper logging:

// src/utils/logger.js
const isDevelopment = process.env.NODE_ENV === 'development';

class Logger {
    info(message, meta = {}) {
        if (isDevelopment) {
            console.log('[INFO]', message, meta);
        }
        // Send to logging service in production
    }

    warn(message, meta = {}) {
        if (isDevelopment) {
            console.warn('[WARN]', message, meta);
        }
        // Send to logging service
    }

    error(message, error, meta = {}) {
        if (isDevelopment) {
            console.error('[ERROR]', message, error, meta);
        }
        // Send to error tracking service (Sentry, etc.)
    }

    debug(message, meta = {}) {
        if (isDevelopment) {
            console.debug('[DEBUG]', message, meta);
        }
    }
}

export const logger = new Logger();

// Usage
import { logger } from '@/utils/logger';

function fetchProducts() {
    logger.info('Fetching products', { page: 1, limit: 10 });
    
    try {
        const products = await api.getProducts();
        logger.info('Products fetched successfully', { count: products.length });
        return products;
    } catch (error) {
        logger.error('Failed to fetch products', error, { page: 1 });
        throw error;
    }
}

🧪 Testing Security

Security Test Examples

// __tests__/security/xss.test.js
import { render, screen } from '@testing-library/react';
import TextLimit from '@/utils/customFunctions/TextLimit';

describe('XSS Prevention', () => {
    it('should sanitize malicious HTML', () => {
        const maliciousInput = '<script>alert("XSS")</script>Hello';
        
        render(<TextLimit value={maliciousInput} maxLength={100} />);
        
        // Script tag should be removed
        expect(screen.queryByText(/script/i)).not.toBeInTheDocument();
        expect(screen.getByText(/Hello/i)).toBeInTheDocument();
    });

    it('should handle img onerror attacks', () => {
        const maliciousInput = '<img src=x onerror="alert(1)">';
        
        render(<TextLimit value={maliciousInput} maxLength={100} />);
        
        // onerror should be removed
        const img = screen.queryByRole('img');
        expect(img?.getAttribute('onerror')).toBeNull();
    });
});

// __tests__/security/auth.test.js
describe('Authentication', () => {
    it('should redirect unauthenticated users', async () => {
        // Mock no auth token
        document.cookie = 'auth_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC';
        
        const { result } = renderHook(() => usePermission('product.view'));
        
        await waitFor(() => {
            expect(result.current.isAuthorized).toBe(false);
        });
    });
});

📚 Additional Resources


Last Updated: January 2026