Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions src/assets/js/components/experience.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Experience section component
*/

import { sanitizeUrl } from '../utils/validator.js';

export const populateExperience = (experience) => {
const experienceContainer = document.getElementById('experience-container');
if (!experienceContainer) return;
Expand All @@ -11,21 +13,16 @@ export const populateExperience = (experience) => {
// The fallback icon now acts as a base layer.
const fallbackIconHTML = `<div class="w-12 h-12 rounded-full bg-gray-100 dark:bg-gray-700 grid place-items-center"><i class="fas fa-suitcase text-2xl text-gray-400 dark:text-gray-500"></i></div>`;

// The image is an overlay that will be removed on error.
const imageHTML = `
<img
src="${exp.logoUrl}"
alt="${exp.company} Logo"
class="company-logo w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-700"
style="position: absolute; top: 0; left: 0;"
onerror="this.remove()"
>`;
// The image will be added via JavaScript to handle errors properly
const imageContainerHTML = exp.logoUrl
? `<div class="company-logo-container" data-logo-url="${sanitizeUrl(exp.logoUrl)}" data-company="${exp.company}"></div>`
: '';

return `
<div class="experience-item mb-6 flex items-start space-x-4">
<div class="flex-shrink-0 mt-1" style="position: relative;">
${fallbackIconHTML}
${exp.logoUrl ? imageHTML : ''}
${imageContainerHTML}
</div>
<div class="flex-grow">
<h3 class="text-xl font-semibold text-gray-800 dark:text-white">${exp.title}</h3>
Expand All @@ -41,4 +38,29 @@ export const populateExperience = (experience) => {
.join('');

experienceContainer.innerHTML = experienceHTML;

// Add company logos with proper error handling (no inline handlers)
const logoContainers = experienceContainer.querySelectorAll('.company-logo-container');
logoContainers.forEach((container) => {
const logoUrl = container.dataset.logoUrl;
const company = container.dataset.company;

if (logoUrl && logoUrl !== '#') {
const img = document.createElement('img');
img.src = logoUrl;
img.alt = `${company} Logo`;
img.className =
'company-logo w-12 h-12 rounded-full object-cover border border-gray-200 dark:border-gray-700';
img.style.position = 'absolute';
img.style.top = '0';
img.style.left = '0';

// Use addEventListener instead of inline onerror
img.addEventListener('error', () => {
img.remove();
});

container.appendChild(img);
}
});
};
14 changes: 8 additions & 6 deletions src/assets/js/components/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Projects section with modern design
*/

import { sanitizeUrl } from '../utils/validator.js';

export const populateProjects = (projects) => {
const projectsContainer = document.getElementById('projects-container');
if (!projectsContainer) return;
Expand Down Expand Up @@ -65,10 +67,10 @@ export const populateProjects = (projects) => {
${
project.link
? `
<a
href="${project.link}"
class="btn btn-primary"
target="_blank"
<a
href="${sanitizeUrl(project.link)}"
class="btn btn-primary"
target="_blank"
rel="noopener noreferrer"
aria-label="View ${project.title}"
>
Expand All @@ -81,8 +83,8 @@ export const populateProjects = (projects) => {
${
project.github
? `
<a
href="${project.github}"
<a
href="${sanitizeUrl(project.github)}"
class="btn btn-outline btn-sm border-gray-300 text-gray-700 bg-white hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:bg-gray-800 dark:hover:bg-gray-700"
target="_blank"
rel="noopener noreferrer"
Expand Down
53 changes: 53 additions & 0 deletions src/assets/js/utils/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* URL and input validation utilities
*/

/**
* Validates if a URL is safe to use in href attributes
* Prevents javascript: and data: URL injection attacks
*
* @param {string} url - The URL to validate
* @returns {boolean} - True if URL is valid and safe
*/
export const isValidUrl = (url) => {
if (!url || typeof url !== 'string') {
return false;
}

try {
const parsed = new URL(url, window.location.origin);
// Only allow http, https, and mailto protocols
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
} catch {
// If URL parsing fails, treat as relative URL
// Check if it starts with / (relative path) or # (anchor)
return url.startsWith('/') || url.startsWith('#');
}
};

/**
* Sanitizes a URL to prevent XSS attacks
* Returns a safe URL or fallback to '#'
*
* @param {string} url - The URL to sanitize
* @returns {string} - Safe URL or '#' if invalid
*/
export const sanitizeUrl = (url) => {
return isValidUrl(url) ? url : '#';
};

/**
* Validates email address format
*
* @param {string} email - The email to validate
* @returns {boolean} - True if email format is valid
*/
export const isValidEmail = (email) => {
if (!email || typeof email !== 'string') {
return false;
}

// Basic email validation regex
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};