feat: Containerize services with Docker Compose (PostgreSQL, Strapi CMS, Express Backend, Nginx Frontend) on custom ports >9000

This commit is contained in:
Vishva
2026-08-03 19:58:10 +05:30
parent 48f8ab9716
commit 6244fd4072
247 changed files with 56672 additions and 169 deletions
+14 -1
View File
@@ -14,6 +14,8 @@ import multer from 'multer';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3001;
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, 'uploads');
const STRAPI_UPLOADS_DIR = path.join(__dirname, '..', 'strapi-cms', 'public', 'uploads');
const PUBLIC_UPLOADS_DIR = path.join(__dirname, '..', 'public', 'uploads');
// Public URL prefix on Apache (e.g. /citpl_website). Leave empty if site is at domain root.
const SITE_BASE = (process.env.SITE_BASE ?? '').replace(/\/$/, '');
const distDir = path.join(__dirname, '..', 'dist');
@@ -42,6 +44,8 @@ function mountApp(base) {
const prefix = base || '';
app.use(`${prefix}/uploads`, express.static(UPLOAD_DIR));
app.use(`${prefix}/uploads`, express.static(STRAPI_UPLOADS_DIR));
app.use(`${prefix}/uploads`, express.static(PUBLIC_UPLOADS_DIR));
app.get(`${prefix}/api/health`, (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
@@ -74,8 +78,14 @@ if (SITE_BASE) {
// Fallback: serve public/assets directly at /assets/ and /citpl_website/assets/
// This ensures cert/logo images load regardless of which base the build used
app.use('/assets', express.static(publicAssetsDir));
app.use('/uploads', express.static(UPLOAD_DIR));
app.use('/uploads', express.static(STRAPI_UPLOADS_DIR));
app.use('/uploads', express.static(PUBLIC_UPLOADS_DIR));
if (SITE_BASE) {
app.use(`${SITE_BASE}/assets`, express.static(publicAssetsDir));
app.use(`${SITE_BASE}/uploads`, express.static(UPLOAD_DIR));
app.use(`${SITE_BASE}/uploads`, express.static(STRAPI_UPLOADS_DIR));
app.use(`${SITE_BASE}/uploads`, express.static(PUBLIC_UPLOADS_DIR));
}
// Return proper 404 for missing static assets (prevents index.html served as JS/CSS)
@@ -95,7 +105,7 @@ app.use((err, _req, res, _next) => {
res.status(500).json({ error: err.message || 'Internal server error', code: 'SERVER_ERROR' });
});
app.listen(PORT, () => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`CITPL Node server on http://localhost:${PORT}`);
console.log(` API: /api/* and ${SITE_BASE || ''}/api/*`);
console.log(` Auth: ADMIN_USERNAME from .env (${process.env.ADMIN_USERNAME || 'admin'})`);
@@ -103,3 +113,6 @@ app.listen(PORT, () => {
console.log(` Static: ${distDir}`);
}
});
// Keep process active in background subshells
setInterval(() => {}, 1000 * 3600);
+42 -14
View File
@@ -1,8 +1,10 @@
import express from 'express';
import multer from 'multer';
import { saveLead, getLeads } from '../db.js';
import { sendContactEmail } from '../services/mailService.js';
import { sendContactEmail, sendCareersEmail, sendStrategyEmail } from '../services/mailService.js';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
async function verifyTurnstile(token, remoteip) {
const secretKey = process.env.TURNSTILE_SECRET_KEY || '1x000000000000000000000000000000AA';
@@ -25,10 +27,10 @@ async function verifyTurnstile(token, remoteip) {
}
}
// POST /api/contact - Handle Contact Us Form submission via SMTP
router.post('/', async (req, res) => {
// POST /api/contact - Handle Contact, Careers, and Strategy Call form submissions via SMTP
router.post('/', upload.single('resume'), async (req, res) => {
try {
const { name, email, phone, mobile, subject, message, interests, type, turnstileToken } = req.body;
const { name, email, phone, mobile, subject, message, interests, type, designation, dateTime, resumeName, turnstileToken } = req.body;
const contactName = name?.trim();
const contactEmail = email?.trim();
@@ -55,17 +57,43 @@ router.post('/', async (req, res) => {
phone: contactPhone,
subject: contactSubject,
message: contactMessage,
designation,
dateTime,
resumeName,
type: type || 'contact'
});
// Attempt sending email via SMTP service
const mailResult = await sendContactEmail({
name: contactName,
email: contactEmail,
phone: contactPhone,
subject: contactSubject,
message: contactMessage
});
// Attempt sending email via SMTP service based on form type
let mailResult = { success: false };
if (type === 'careers') {
mailResult = await sendCareersEmail({
name: contactName,
email: contactEmail,
phone: contactPhone,
resumeName: req.file ? req.file.originalname : (resumeName || 'Resume Uploaded'),
message: contactMessage,
file: req.file
});
} else if (type === 'strategy') {
mailResult = await sendStrategyEmail({
name: contactName,
email: contactEmail,
phone: contactPhone,
designation: designation || 'N/A',
dateTime: dateTime || 'N/A',
interests: interests || 'General Strategy Consultation',
message: contactMessage
});
} else {
mailResult = await sendContactEmail({
name: contactName,
email: contactEmail,
phone: contactPhone,
subject: contactSubject,
message: contactMessage
});
}
if (!mailResult.success) {
// If SMTP is not yet configured or fails, return 200 with lead saved & warning
@@ -74,7 +102,7 @@ router.post('/', async (req, res) => {
saved: true,
leadId: lead.id,
smtpError: mailResult.error,
message: 'Your message was saved successfully. Note: SMTP email dispatch requires SMTP credentials in .env.'
message: 'Your request was saved successfully. Note: SMTP email dispatch requires SMTP credentials in .env.'
});
}
@@ -83,7 +111,7 @@ router.post('/', async (req, res) => {
saved: true,
leadId: lead.id,
messageId: mailResult.messageId,
message: 'Message sent successfully via SMTP!'
message: 'Request sent successfully via SMTP!'
});
} catch (error) {
console.error('[Contact Route Error]', error);
+12 -1
View File
@@ -1,17 +1,28 @@
import { Router } from 'express';
import { getContent, updateSection, updateAllContent } from '../db.js';
import { requireAuth } from '../auth.js';
import { getStrapiBlogs } from '../strapi.js';
import seedContent from '../seed/default-content.json' with { type: 'json' };
const VALID_SECTIONS = Object.keys(seedContent);
const router = Router();
router.get('/', (_req, res) => {
router.get('/', async (_req, res) => {
const { content, updatedAt } = getContent();
const strapiItems = await getStrapiBlogs();
if (strapiItems && content.insights) {
content.insights.items = strapiItems;
}
res.json({ ...content, _updatedAt: updatedAt });
});
router.get('/blogs', async (_req, res) => {
const { content } = getContent();
const strapiItems = await getStrapiBlogs();
res.json(strapiItems || content.insights?.items || []);
});
router.put('/', requireAuth, (req, res) => {
const { _updatedAt, ...content } = req.body;
const updatedAt = updateAllContent(content);
+315 -60
View File
@@ -5,11 +5,31 @@
"copyright": "Copyrights reserved to CAVIN INFOTECH"
},
"navigation": [
{ "label": "Home", "href": "#home", "type": "hash" },
{ "label": "About Us", "href": "#aboutus", "type": "hash" },
{ "label": "Products", "href": "#products", "type": "hash" },
{ "label": "Careers", "href": "#careers", "type": "modal" },
{ "label": "Contact Us", "href": "#contactus", "type": "hash" }
{
"label": "Home",
"href": "#home",
"type": "hash"
},
{
"label": "About Us",
"href": "#aboutus",
"type": "hash"
},
{
"label": "Products",
"href": "#products",
"type": "hash"
},
{
"label": "Careers",
"href": "#careers",
"type": "modal"
},
{
"label": "Contact Us",
"href": "#contactus",
"type": "hash"
}
],
"hero": {
"video": "/assets/citpl-hero.mp4",
@@ -21,10 +41,22 @@
"secondaryCta": "Book a Strategy Call"
},
"metrics": [
{ "value": "100+", "label": "Clients" },
{ "value": "5+", "label": "Years of Global Operations" },
{ "value": "4", "label": "Group Companies" },
{ "value": "2", "label": "Global Offices" }
{
"value": "100+",
"label": "Clients"
},
{
"value": "5+",
"label": "Years of Global Operations"
},
{
"value": "4",
"label": "Group Companies"
},
{
"value": "2",
"label": "Global Offices"
}
],
"about": {
"heading": "Trusted Technology Partner",
@@ -37,26 +69,71 @@
"subheading": "from various industries",
"body": "Professionals Trust Our Solutions And Complete Their Journeys.",
"logos": [
{ "name": "CavinKare", "image": "/assets/Frame 27.svg" },
{ "name": "Valeo", "image": "/assets/Frame 28.svg" },
{ "name": "Polyhose", "image": "/assets/Frame 30.svg" },
{ "name": "ACA Global", "image": "/assets/Frame 31.svg" },
{ "name": "Dawn Pictures", "image": "/assets/Frame 32.svg" },
{ "name": "MRF", "image": "/assets/Frame 33.svg" },
{ "name": "Chola MS", "image": "/assets/Frame 34.svg" },
{ "name": "Hero", "image": "/assets/Frame 35.svg" },
{ "name": "BorgWarner", "image": "/assets/Frame 36.svg" },
{ "name": "KONE", "image": "/assets/kone.png" }
{
"name": "CavinKare",
"image": "/assets/Frame 27.svg"
},
{
"name": "Valeo",
"image": "/assets/Frame 28.svg"
},
{
"name": "Polyhose",
"image": "/assets/Frame 30.svg"
},
{
"name": "ACA Global",
"image": "/assets/Frame 31.svg"
},
{
"name": "Dawn Pictures",
"image": "/assets/Frame 32.svg"
},
{
"name": "MRF",
"image": "/assets/Frame 33.svg"
},
{
"name": "Chola MS",
"image": "/assets/Frame 34.svg"
},
{
"name": "Hero",
"image": "/assets/Frame 35.svg"
},
{
"name": "BorgWarner",
"image": "/assets/Frame 36.svg"
},
{
"name": "KONE",
"image": "/assets/kone.png"
}
]
},
"certifications": {
"heading": "Certified to build what enterprises trust",
"items": [
{ "image": "/assets/Certifications Logo. 01.svg", "alt": "ISO 27001:2022 Certification" },
{ "image": "/assets/Certifications Logo. 02.svg", "alt": "ISO 9001:2015 Certification" },
{ "image": "/assets/Certifications Logo. 03.svg", "alt": "SOC 2 Type II Certification" },
{ "image": "/assets/Certifications Logo. 04.svg", "alt": "Nasscom Member Logo" },
{ "image": "/assets/Certifications Logo. 05.svg", "alt": "Great Place to Work Certified 2026 INDIA" }
{
"image": "/assets/Certifications Logo. 01.svg",
"alt": "ISO 27001:2022 Certification"
},
{
"image": "/assets/Certifications Logo. 02.svg",
"alt": "ISO 9001:2015 Certification"
},
{
"image": "/assets/Certifications Logo. 03.svg",
"alt": "SOC 2 Type II Certification"
},
{
"image": "/assets/Certifications Logo. 04.svg",
"alt": "Nasscom Member Logo"
},
{
"image": "/assets/Certifications Logo. 05.svg",
"alt": "Great Place to Work Certified 2026 INDIA"
}
]
},
"services": {
@@ -168,9 +245,18 @@
}
],
"images": [
{ "src": "/assets/whyus_arab_meeting.jpg", "alt": "Corporate collaboration" },
{ "src": "/assets/whyus_vr_man.jpg", "alt": "VR technology" },
{ "src": "/assets/whyus_teamwork_user.jpg", "alt": "Teamwork technology" }
{
"src": "/assets/whyus_arab_meeting.jpg",
"alt": "Corporate collaboration"
},
{
"src": "/assets/whyus_vr_man.jpg",
"alt": "VR technology"
},
{
"src": "/assets/whyus_teamwork_user.jpg",
"alt": "Teamwork technology"
}
]
},
"gallery": {
@@ -178,12 +264,54 @@
"title": "Real Transformations. Proven Business Outcomes.",
"subtitle": "From transformation stories to team innovation, explore how we build future-ready experiences.",
"items": [
{ "id": 1, "title": "Relentless Brand Growth & Innovation", "category": "Case Study", "excerpt": "Discover how we engineered a scalable digital platform that boosted brand engagement by 150% and accelerated global market reach.", "image": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80", "readTime": "5 Min Read" },
{ "id": 2, "title": "How We Transformed Billing at TTK", "category": "Transformation", "excerpt": "Transitioning legacy paper billing to an automated IoT-driven workflow, reducing checkout delays and improving customer satisfaction.", "image": "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=800&q=80", "readTime": "8 Min Read" },
{ "id": 3, "title": "How We Transformed Retail Operations", "category": "Retail", "excerpt": "Unifying online inventory with physical retail networks, enabling smart warehouse tracking and seamless order fulfillment.", "image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?auto=format&fit=crop&w=800&q=80", "readTime": "6 Min Read" },
{ "id": 4, "title": "How We Transformed Enterprise Workflows", "category": "Workflow", "excerpt": "Re-engineering internal communication channels and operations with Gen AI assistants, saving thousands of operational hours.", "image": "https://images.unsplash.com/photo-1531538606174-0f90ff5dce83?auto=format&fit=crop&w=800&q=80", "readTime": "10 Min Read" },
{ "id": 5, "title": "Digital Product Showcase", "category": "Showcase", "excerpt": "A curation of sleek web interfaces, user-centric mobile applications, and custom enterprise portals designed by our UI/UX experts.", "image": "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?auto=format&fit=crop&w=800&q=80", "readTime": "7 Min Read" },
{ "id": 6, "title": "Innovation Lab", "category": "Innovation", "excerpt": "Step inside our workshop where we prototype Agentic AI, smart hardware sensors, and computer vision systems for next-generation industries.", "image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80", "readTime": "12 Min Read" }
{
"id": 1,
"title": "Relentless Brand Growth & Innovation",
"category": "Case Study",
"excerpt": "Discover how we engineered a scalable digital platform that boosted brand engagement by 150% and accelerated global market reach.",
"image": "https://images.unsplash.com/photo-1460925895917-afdab827c52f?auto=format&fit=crop&w=800&q=80",
"readTime": "5 Min Read"
},
{
"id": 2,
"title": "How We Transformed Billing at TTK",
"category": "Transformation",
"excerpt": "Transitioning legacy paper billing to an automated IoT-driven workflow, reducing checkout delays and improving customer satisfaction.",
"image": "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=800&q=80",
"readTime": "8 Min Read"
},
{
"id": 3,
"title": "How We Transformed Retail Operations",
"category": "Retail",
"excerpt": "Unifying online inventory with physical retail networks, enabling smart warehouse tracking and seamless order fulfillment.",
"image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?auto=format&fit=crop&w=800&q=80",
"readTime": "6 Min Read"
},
{
"id": 4,
"title": "How We Transformed Enterprise Workflows",
"category": "Workflow",
"excerpt": "Re-engineering internal communication channels and operations with Gen AI assistants, saving thousands of operational hours.",
"image": "https://images.unsplash.com/photo-1531538606174-0f90ff5dce83?auto=format&fit=crop&w=800&q=80",
"readTime": "10 Min Read"
},
{
"id": 5,
"title": "Digital Product Showcase",
"category": "Showcase",
"excerpt": "A curation of sleek web interfaces, user-centric mobile applications, and custom enterprise portals designed by our UI/UX experts.",
"image": "https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?auto=format&fit=crop&w=800&q=80",
"readTime": "7 Min Read"
},
{
"id": 6,
"title": "Innovation Lab",
"category": "Innovation",
"excerpt": "Step inside our workshop where we prototype Agentic AI, smart hardware sensors, and computer vision systems for next-generation industries.",
"image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80",
"readTime": "12 Min Read"
}
]
},
"products": {
@@ -196,8 +324,14 @@
"description": "Comprehensive enterprise AI for intelligent automation, predictive insights, Generative AI, Agentic AI, and conversational intelligence.",
"image": "/assets/Nebula Img.png",
"stats": [
{ "value": "60 %", "label": "Faster Process Automation" },
{ "value": "24/7", "label": "AI Driven Assistance" }
{
"value": "60 %",
"label": "Faster Process Automation"
},
{
"value": "24/7",
"label": "AI Driven Assistance"
}
]
},
{
@@ -205,8 +339,14 @@
"description": "AI-powered Industrial IoT platform with 16+ modules for OEE, energy management, traceability, AI vision, and smart manufacturing excellence.",
"image": "/assets/Prodmax Img.png",
"stats": [
{ "value": "20%+", "label": "OEE Improvement" },
{ "value": "15%", "label": "Reduction in Downtime" }
{
"value": "20%+",
"label": "OEE Improvement"
},
{
"value": "15%",
"label": "Reduction in Downtime"
}
]
},
{
@@ -214,7 +354,10 @@
"description": "AI-driven HRMS for intelligent recruitment, workforce scheduling, engagement, and collaboration.",
"image": "/assets/Budgie Img.png",
"stats": [
{ "value": "40 %", "label": "Faster HR Operations" }
{
"value": "40 %",
"label": "Faster HR Operations"
}
]
}
]
@@ -223,13 +366,81 @@
"title": "Explore the Latest in Technology & Innovation",
"subtitle": "Stay ahead with insights, stories, and updates on digital transformation, AI, automation, product design, and enterprise technology.",
"items": [
{ "category": "AI & Analytics", "title": "How AI is Reshaping Enterprise Decision Making", "date": "June 15, 2026", "image": "https://images.unsplash.com/photo-1620712943543-bcc4688e7485?auto=format&fit=crop&w=800&q=80", "excerpt": "Explore how deep learning models and agentic decision trees are replacing traditional business intelligence systems to drive proactive operations." },
{ "category": "Digital Strategy", "title": "Why Digital Transformation Needs More Than Technology", "date": "June 02, 2026", "image": "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80", "excerpt": "Technology is only half the battle. True organizational agility requires architectural alignment, structural changes, and cultural evolution." },
{ "category": "Product Design", "title": "Designing Enterprise Software Users Actually Love", "date": "May 18, 2026", "image": "https://images.unsplash.com/photo-1586717791821-3f44a563fa4c?auto=format&fit=crop&w=800&q=80", "excerpt": "Applying consumer-grade user experience principles to complex B2B systems reduces training costs and accelerates daily workflows." },
{ "category": "Automation", "title": "The Future of Automation in Business Workflows", "date": "May 05, 2026", "image": "https://images.unsplash.com/photo-1485827404703-89b55fcc595e?auto=format&fit=crop&w=800&q=80", "excerpt": "From robotic process automation (RPA) to generative AI pipelines, learn how workflow automation is changing modern business scaling." },
{ "category": "Cloud Architecture", "title": "Building Scalable Cloud-Native Infrastructure for Enterprises", "date": "April 22, 2026", "image": "https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80", "excerpt": "Microservices, Kubernetes, and serverless patterns are redefining how large-scale enterprise systems handle performance at unpredictable loads." },
{ "category": "Cybersecurity", "title": "Zero Trust Security Models in the Age of Remote Work", "date": "April 08, 2026", "image": "https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80", "excerpt": "As perimeter-based security crumbles, zero trust frameworks enforce identity verification at every layer — from endpoint to data center." },
{ "category": "Data Engineering", "title": "Real-Time Data Pipelines That Power Smarter Decisions", "date": "March 25, 2026", "image": "https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=800&q=80", "excerpt": "Event-driven architectures and streaming platforms like Apache Kafka enable organizations to act on data the moment it is generated." }
{
"id": 60,
"category": "AI & Data",
"title": "Clean Content Form Test Blog",
"excerpt": "Testing clean editing interface with hidden auto-fill fields.",
"content": "# Clean Content Editing\n\nAuthors focus only on writing without form clutter.",
"readTime": "4 min read",
"date": "Jul 30, 2026",
"image": "/uploads/arvindh_agentic_ai_3ea158c92b.jpg",
"slug": "clean-content-form-test-blog",
"author": "Dr. Arvindh Ramachandran",
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
"editor": null,
"editorDesignation": null,
"tags": [
"CleanUI",
"AutoFill"
]
},
{
"id": 62,
"category": "Industrial IoT",
"title": "Blog entry 123",
"excerpt": "Blog Entry 123",
"content": "A salon-exclusive waxing system inspired by Korean skincare. Infused with skin-loving actives, Dermaxix K-Line Glass Wax delivers a smooth, reflective finish for radiant, glass-like skin.\n\nSmooth Icon\nSMOOTH\nSilky-smooth finish\nevery time.\n\nRadiant Icon\nRADIANT\nReflective glow that\nlasts.\n\n",
"readTime": "5 min read",
"date": "Jul 30, 2026",
"image": "/uploads/Quality_Control_Collage_33d26fa0cc.jpg",
"slug": "blog-entry-123",
"author": "Dr. Arvindh Ramachandran",
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
"editor": null,
"editorDesignation": null,
"tags": []
},
{
"id": 63,
"category": "AR/VR & Immersive Tech",
"title": "Building Autonomous LLM Pipeline Workflows for Financial Services",
"excerpt": "Designing fault-tolerant, audit-ready LLM orchestration layers for complex risk assessment and automated compliance.",
"content": "# Building Autonomous LLM Pipelines\n\nFinancial services demand uncompromising accuracy, auditability, and speed when deploying LLM workflows...",
"readTime": "5 min read",
"date": "Jul 31, 2026",
"image": "/assets/arvindh_llm_financial.jpg",
"slug": "building-autonomous-llm-pipeline-workflows-for-financial-services",
"author": "Dr. Arvindh Ramachandran",
"authorDesignation": "Chief AI Architect & Head of Data Engineering",
"editor": "Jainaressh BC",
"editorDesignation": "Senior Editor - Enterprise Software & Cloud",
"tags": [
"LLM",
"FinancialTech",
"Compliance"
]
},
{
"id": 64,
"category": "Operation Technology",
"title": "Zero-Trust Security Protocols for Connected SCADA & PLC Industrial Networks",
"excerpt": "Implementing micro-segmentation and strict cryptographic handshake verification across industrial control systems.",
"content": "# Zero-Trust Security for SCADA & PLCs\n\nLegacy PLCs were built without authentication. Zero-trust network segmentation safeguards operational continuity...",
"readTime": "6 min read",
"date": "Jul 31, 2026",
"image": "/assets/whyus_arab_meeting.jpg",
"slug": "zero-trust-security-protocols-for-connected-scada-plc-industrial-networks",
"author": "Vikram Sen",
"authorDesignation": "Senior Editor - Industrial Operations & OT Tech",
"editor": "Jainaressh BC",
"editorDesignation": "Senior Editor - Industrial Operations & OT Tech",
"tags": [
"ZeroTrust",
"Cybersecurity",
"PLC"
]
}
]
},
"footer": {
@@ -241,25 +452,69 @@
"secondaryButton": "Let's Solve Your Biggest Challenge →"
},
"quickLinks": [
{ "label": "Home", "href": "#home" },
{ "label": "About Us", "href": "#certifications" },
{ "label": "Our Services", "href": "#services" }
{
"label": "Home",
"href": "#home"
},
{
"label": "About Us",
"href": "#certifications"
},
{
"label": "Our Services",
"href": "#services"
},
{
"label": "Contact Us",
"href": "#contactus"
}
],
"productLinks": [
{ "label": "Nebula AI platform", "href": "#products" },
{ "label": "Prodmax", "href": "#products" },
{ "label": "Budgie", "href": "#products" }
{
"label": "Nebula AI Platform",
"href": "#products"
},
{
"label": "Prodmax",
"href": "#products"
},
{
"label": "Budgie",
"href": "#products"
}
],
"social": [
{ "platform": "Facebook", "url": "https://www.facebook.com/cavininfotech1" },
{ "platform": "X", "url": "https://x.com/cavin_infotech" },
{ "platform": "Instagram", "url": "https://www.instagram.com/cavin_infotech" },
{ "platform": "LinkedIn", "url": "https://www.linkedin.com/company/cavin-infotech/" },
{ "platform": "YouTube", "url": "https://youtube.com/@cavin_infotech" }
{
"platform": "Facebook",
"url": "https://www.facebook.com/cavininfotech1"
},
{
"platform": "X",
"url": "https://x.com/cavin_infotech"
},
{
"platform": "Instagram",
"url": "https://www.instagram.com/cavin_infotech"
},
{
"platform": "LinkedIn",
"url": "https://www.linkedin.com/company/cavin-infotech/"
},
{
"platform": "YouTube",
"url": "https://youtube.com/@cavin_infotech"
}
],
"legal": [
{ "label": "Privacy Policy", "href": "#" },
{ "label": "Terms & Conditions", "href": "#" }
{
"label": "Privacy Policy",
"href": "#"
},
{
"label": "Terms & Conditions",
"href": "#"
}
]
}
}
},
"updatedAt": "2026-07-30T14:33:08.281Z"
}
+213 -6
View File
@@ -29,7 +29,7 @@ export function createSmtpTransporter() {
}
/**
* Sends a contact form email via SMTP using nodemailer.
* Sends a general contact form email via SMTP using nodemailer.
*/
export async function sendContactEmail({ name, email, phone, subject, message }) {
const transporter = createSmtpTransporter();
@@ -73,7 +73,7 @@ export async function sendContactEmail({ name, email, phone, subject, message })
</div>
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Contact Us Page
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Website
</div>
</div>
`;
@@ -93,7 +93,6 @@ Submitted on: ${new Date().toLocaleString()}
`;
if (!transporter) {
console.warn('[SMTP Service] SMTP credentials not set in environment (SMTP_HOST, SMTP_USER, SMTP_PASS).');
return {
success: false,
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
@@ -109,11 +108,219 @@ Submitted on: ${new Date().toLocaleString()}
text: textContent,
html: htmlContent
});
console.log('[SMTP Service] Contact email sent via SMTP:', info.messageId);
return { success: true, messageId: info.messageId };
} catch (err) {
console.error('[SMTP Service] Failed to send email via SMTP:', err);
return { success: false, error: err.message || 'SMTP sending failed' };
}
}
/**
* Sends a Careers application form email via SMTP using nodemailer.
*/
export async function sendCareersEmail({ name, email, phone, resumeName, message, file }) {
const transporter = createSmtpTransporter();
const toEmail = process.env.SMTP_TO || process.env.SMTP_USER || 'info@cavinfotech.com';
const noReplyEmail = process.env.SMTP_FROM || `"Cavin Infotech Careers" <${process.env.SMTP_USER || 'info@cavinfotech.com'}>`;
const mailSubject = `[Careers Application] New Application from ${name}`;
// Internal notification HTML (sent to the team)
const internalHtml = `
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">New Careers / Job Application</p>
</div>
<div style="padding: 24px 0;">
<table style="width: 100%; border-collapse: collapse; color: #e2e8f0; font-size: 15px;">
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8; width: 140px;">Applicant Name:</td>
<td style="padding: 10px 0; color: #ffffff;">${name}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Email:</td>
<td style="padding: 10px 0;"><a href="mailto:${email}" style="color: #38bdf8; text-decoration: none;">${email}</a></td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Phone No:</td>
<td style="padding: 10px 0; color: #ffffff;">${phone || 'Not provided'}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Resume File:</td>
<td style="padding: 10px 0; color: #ffaa00; font-weight: 600;">${resumeName || 'Attached'}</td>
</tr>
</table>
${message ? `
<div style="margin-top: 24px; padding: 20px; background-color: #0b1528; border-left: 4px solid #38bdf8; border-radius: 8px;">
<h4 style="margin: 0 0 10px 0; color: #94a3b8; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;">Notes / Message:</h4>
<p style="margin: 0; color: #f1f5f9; font-size: 15px; line-height: 1.6; white-space: pre-line;">${message}</p>
</div>
` : ''}
</div>
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Careers Form
</div>
</div>
`;
// Auto-reply HTML (sent to the applicant)
const autoReplyHtml = `
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">Application Received — Thank You!</p>
</div>
<div style="padding: 28px 0;">
<p style="font-size: 16px; color: #e2e8f0; line-height: 1.7; margin: 0 0 16px 0;">
Dear <strong style="color: #ffffff;">${name}</strong>,
</p>
<p style="font-size: 15px; color: #cbd5e1; line-height: 1.7; margin: 0 0 16px 0;">
Thank you for applying to <strong style="color: #38bdf8;">Cavin Infotech</strong>. We have successfully received your application and resume.
</p>
<p style="font-size: 15px; color: #cbd5e1; line-height: 1.7; margin: 0 0 24px 0;">
Our team will review your profile and reach out to you if your qualifications match our current openings. We appreciate your interest in joining our team!
</p>
<div style="background: linear-gradient(135deg, rgba(3, 135, 218, 0.15) 0%, rgba(2, 7, 16, 0.6) 100%); border: 1px solid rgba(56, 189, 248, 0.2); border-radius: 12px; padding: 20px 24px; margin-bottom: 24px;">
<p style="margin: 0 0 8px 0; font-size: 13px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em;">Application Summary</p>
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Name:</strong> ${name}</p>
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Email:</strong> ${email}</p>
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Phone:</strong> ${phone || 'Not provided'}</p>
<p style="margin: 4px 0; font-size: 14px; color: #e2e8f0;"><strong style="color: #38bdf8;">Resume:</strong> ${resumeName || 'Submitted'}</p>
</div>
<p style="font-size: 13px; color: #64748b; line-height: 1.6; margin: 0;">
This is an automated confirmation email. Please do not reply to this message. For enquiries, contact us at
<a href="mailto:info@cavinfotech.com" style="color: #38bdf8; text-decoration: none;">info@cavinfotech.com</a>.
</p>
</div>
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
© ${new Date().getFullYear()} Cavin Infotech. All rights reserved.
</div>
</div>
`;
if (!transporter) {
return {
success: false,
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
};
}
try {
// 1. Send internal team notification (with resume attachment)
const mailOptions = {
from: noReplyEmail,
to: toEmail,
replyTo: `"${name}" <${email}>`,
subject: mailSubject,
text: `New Careers Application:\nName: ${name}\nEmail: ${email}\nPhone: ${phone}\nResume: ${resumeName}`,
html: internalHtml
};
if (file) {
mailOptions.attachments = [
{
filename: file.originalname,
content: file.buffer
}
];
}
const info = await transporter.sendMail(mailOptions);
// 2. Send auto-reply confirmation to the applicant
await transporter.sendMail({
from: noReplyEmail,
to: email,
subject: `We've received your application — Cavin Infotech`,
text: `Dear ${name},\n\nThank you for applying to Cavin Infotech. We have received your application and will review it shortly.\n\nThis is an automated confirmation. Please do not reply to this email.\n\n© ${new Date().getFullYear()} Cavin Infotech`,
html: autoReplyHtml
});
return { success: true, messageId: info.messageId };
} catch (err) {
return { success: false, error: err.message || 'SMTP sending failed' };
}
}
/**
* Sends a Strategy Call booking email via SMTP using nodemailer.
*/
export async function sendStrategyEmail({ name, email, phone, designation, dateTime, interests, message }) {
const transporter = createSmtpTransporter();
const toEmail = process.env.SMTP_TO || process.env.SMTP_USER || 'info@cavinfotech.com';
const fromEmail = process.env.SMTP_FROM || `"Cavin Infotech Strategy Call" <${process.env.SMTP_USER || 'info@cavinfotech.com'}>`;
const mailSubject = `[Strategy Call Booking] New Session Request from ${name}`;
const htmlContent = `
<div style="font-family: 'Segoe UI', Helvetica, Arial, sans-serif; background-color: #020710; color: #f8fafc; padding: 40px 20px; max-width: 650px; margin: 0 auto; border-radius: 16px; border: 1px solid #1e293b;">
<div style="text-align: center; padding-bottom: 24px; border-bottom: 1px solid #334155;">
<h2 style="color: #38bdf8; font-size: 24px; margin: 0; font-weight: 700;">Cavin Infotech</h2>
<p style="color: #94a3b8; font-size: 14px; margin-top: 6px;">New Strategy Call Session Request</p>
</div>
<div style="padding: 24px 0;">
<table style="width: 100%; border-collapse: collapse; color: #e2e8f0; font-size: 15px;">
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8; width: 160px;">Client Name:</td>
<td style="padding: 10px 0; color: #ffffff;">${name}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Email:</td>
<td style="padding: 10px 0;"><a href="mailto:${email}" style="color: #38bdf8; text-decoration: none;">${email}</a></td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Mobile:</td>
<td style="padding: 10px 0; color: #ffffff;">${phone || 'Not provided'}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Designation:</td>
<td style="padding: 10px 0; color: #ffffff;">${designation || 'Not provided'}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Preferred Slot:</td>
<td style="padding: 10px 0; color: #ffaa00; font-weight: 600;">${dateTime || 'Flexible'}</td>
</tr>
<tr>
<td style="padding: 10px 0; font-weight: 600; color: #38bdf8;">Areas of Interest:</td>
<td style="padding: 10px 0; color: #ffffff;">${interests || 'General Consultation'}</td>
</tr>
</table>
</div>
<div style="text-align: center; padding-top: 20px; border-top: 1px solid #334155; color: #64748b; font-size: 12px;">
Submitted on ${new Date().toLocaleString()} from Cavin Infotech Book a Strategy Call Modal
</div>
</div>
`;
if (!transporter) {
return {
success: false,
error: 'SMTP credentials not configured on the server. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env file.'
};
}
try {
const info = await transporter.sendMail({
from: fromEmail,
to: toEmail,
replyTo: `"${name}" <${email}>`,
subject: mailSubject,
text: `New Strategy Call Request:\nName: ${name}\nEmail: ${email}\nMobile: ${phone}\nDesignation: ${designation}\nPreferred Slot: ${dateTime}\nInterests: ${interests}`,
html: htmlContent
});
return { success: true, messageId: info.messageId };
} catch (err) {
return { success: false, error: err.message || 'SMTP sending failed' };
}
}
+180
View File
@@ -0,0 +1,180 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { DatabaseSync } from 'node:sqlite';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const strapiDbPath = path.join(__dirname, '..', 'strapi-cms', '.tmp', 'data.db');
const strapiUploadsDir = path.join(__dirname, '..', 'strapi-cms', 'public', 'uploads');
const targetPublicUploads = path.join(__dirname, '..', 'public', 'uploads');
const targetServerUploads = path.join(__dirname, '..', 'server', 'uploads');
function syncUploads() {
try {
if (!fs.existsSync(strapiUploadsDir)) return;
if (!fs.existsSync(targetPublicUploads)) fs.mkdirSync(targetPublicUploads, { recursive: true });
if (!fs.existsSync(targetServerUploads)) fs.mkdirSync(targetServerUploads, { recursive: true });
const files = fs.readdirSync(strapiUploadsDir);
files.forEach((f) => {
if (f === '.gitkeep') return;
const srcFile = path.join(strapiUploadsDir, f);
const destPublic = path.join(targetPublicUploads, f);
const destServer = path.join(targetServerUploads, f);
if (!fs.existsSync(destPublic)) fs.copyFileSync(srcFile, destPublic);
if (!fs.existsSync(destServer)) fs.copyFileSync(srcFile, destServer);
});
} catch (e) {
console.warn('Note: Upload sync warning:', e.message);
}
}
export async function getStrapiBlogs() {
syncUploads();
// 1. Primary: Fetch live blogs directly from Strapi REST API (PostgreSQL / Live Strapi DB)
try {
const strapiBaseUrl = process.env.STRAPI_URL || 'http://127.0.0.1:1337';
const response = await fetch(`${strapiBaseUrl}/api/blogs?populate=*`, {
headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(3000)
});
if (response.ok) {
const json = await response.json();
const items = json.data || [];
if (Array.isArray(items) && items.length > 0) {
const blogs = items.map((b) => {
let coverUrl = b.coverImage?.url || b.ogImage?.url || null;
if (!coverUrl) {
const cat = (b.category || '').toLowerCase();
if (b.title?.includes('LLM')) coverUrl = '/assets/arvindh_llm_financial.jpg';
else if (b.title?.includes('Supply Chain')) coverUrl = '/assets/arvindh_supply_chain.jpg';
else if (b.title?.includes('PRODMAX')) coverUrl = '/assets/prodmax_dashboard.png';
else if (b.title?.includes('Incidents') || b.title?.includes('VR')) coverUrl = '/assets/whyus_vr_man.jpg';
else if (b.title?.includes('SCADA') || b.title?.includes('IT/OT')) coverUrl = '/assets/whyus_arab_meeting.jpg';
else if (b.title?.includes('Cleanroom')) coverUrl = 'https://images.unsplash.com/photo-1592478411213-6153e4ebc07d?auto=format&fit=crop&w=800&q=80';
else if (b.title?.includes('Spatial') || b.title?.includes('Maintenance')) coverUrl = '/assets/whyus_hologram.jpg';
else if (b.title?.includes('Energy')) coverUrl = '/assets/whyus_teamwork.png';
else if (b.title?.includes('Telemetry') || b.title?.includes('Sensor')) coverUrl = 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&w=800&q=80';
else if (b.title?.includes('Monoliths') || b.title?.includes('Micro-Frontends')) coverUrl = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
else if (b.title?.includes('Zero-Trust')) coverUrl = 'https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80';
else if (b.title?.includes('Multi-Cloud')) coverUrl = 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80';
else if (cat.includes('ai')) coverUrl = '/assets/arvindh_agentic_ai.jpg';
else coverUrl = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
}
const rawDate = b.publishDate || b.publishedAt || b.createdAt;
const dateStr = rawDate
? new Date(rawDate).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
return {
id: b.id || b.documentId,
documentId: b.documentId,
category: b.category || 'Technology',
title: b.title,
excerpt: b.excerpt || '',
content: b.content || '',
readTime: b.readTime || '5 min read',
date: dateStr,
image: coverUrl,
slug: b.slug,
author: typeof b.author === 'string' ? b.author : (b.author?.firstname ? `${b.author.firstname} ${b.author.lastname || ''}`.trim() : (b.author?.username || 'Dr. Arvindh Ramachandran')),
authorDesignation: b.authorDesignation || 'Chief AI Architect & Head of Data Engineering',
editor: typeof b.editor === 'string' ? b.editor : (b.editor?.firstname ? `${b.editor.firstname} ${b.editor.lastname || ''}`.trim() : (b.editor?.username || null)),
editorDesignation: b.editorDesignation || null,
tags: b.tags || [],
metaTitle: b.metaTitle || null,
metaDescription: b.metaDescription || null,
keywords: b.keywords || null,
canonicalURL: b.canonicalURL || null
};
});
if (blogs.length > 0) return blogs;
}
}
} catch (apiErr) {
console.warn('Note: Live Strapi REST API fetch bypassed/failed, trying SQLite fallback:', apiErr.message);
}
// 2. Fallback: SQLite `.tmp/data.db` if Strapi API is offline
try {
if (!fs.existsSync(strapiDbPath)) return null;
const db = new DatabaseSync(strapiDbPath);
const rows = db.prepare('SELECT * FROM blogs WHERE published_at IS NOT NULL ORDER BY id ASC').all();
const mediaLinks = db.prepare("SELECT * FROM files_related_mph WHERE related_type = 'api::blog.blog'").all();
const files = db.prepare('SELECT * FROM files').all();
const adminUsers = db.prepare('SELECT id, firstname, lastname FROM admin_users').all();
const userMap = {};
adminUsers.forEach((u) => {
userMap[u.id] = `${u.firstname || ''} ${u.lastname || ''}`.trim();
});
const seenSlugs = new Set();
const blogs = [];
rows.forEach((b) => {
if (!b.title || b.title.toLowerCase().startsWith('test 3')) return;
if (seenSlugs.has(b.slug)) return;
seenSlugs.add(b.slug);
const link = mediaLinks.find((l) => l.related_id === b.id);
const file = link ? files.find((f) => f.id === link.file_id) : null;
let image = file ? file.url : null;
if (!image) {
const cat = (b.category || '').toLowerCase();
if (b.title.includes('LLM')) image = '/assets/arvindh_llm_financial.jpg';
else if (b.title.includes('Supply Chain')) image = '/assets/arvindh_supply_chain.jpg';
else if (b.title.includes('PRODMAX')) image = '/assets/prodmax_dashboard.png';
else if (b.title.includes('Incidents') || b.title.includes('VR')) image = '/assets/whyus_vr_man.jpg';
else if (b.title.includes('SCADA') || b.title.includes('IT/OT')) image = '/assets/whyus_arab_meeting.jpg';
else if (b.title.includes('Cleanroom')) image = 'https://images.unsplash.com/photo-1592478411213-6153e4ebc07d?auto=format&fit=crop&w=800&q=80';
else if (b.title.includes('Spatial') || b.title.includes('Maintenance')) image = '/assets/whyus_hologram.jpg';
else if (b.title.includes('Energy')) image = '/assets/whyus_teamwork.png';
else if (b.title.includes('Telemetry') || b.title.includes('Sensor')) image = 'https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?auto=format&fit=crop&w=800&q=80';
else if (b.title.includes('Monoliths') || b.title.includes('Micro-Frontends')) image = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
else if (b.title.includes('Zero-Trust')) image = 'https://images.unsplash.com/photo-1563986768494-4dee2763ff3f?auto=format&fit=crop&w=800&q=80';
else if (b.title.includes('Multi-Cloud')) image = 'https://images.unsplash.com/photo-1544197150-b99a580bb7a8?auto=format&fit=crop&w=800&q=80';
else if (cat.includes('ai')) image = '/assets/arvindh_agentic_ai.jpg';
else image = 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80';
}
const dateStr = b.published_at
? new Date(Number(b.published_at)).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: new Date(Number(b.created_at || Date.now())).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
const author = userMap[b.created_by_id] || 'Dr. Arvindh Ramachandran';
const authorRole = b.author_designation || 'Chief AI Architect & Head of Data Engineering';
const hasEditor = Boolean(b.editor_designation || (b.updated_by_id && b.updated_by_id !== b.created_by_id));
const editor = hasEditor ? (userMap[b.updated_by_id] && userMap[b.updated_by_id] !== author ? userMap[b.updated_by_id] : 'Cavin Editorial Board') : null;
const editorRole = b.editor_designation || (hasEditor ? 'Senior Technical Reviewer' : null);
blogs.push({
id: b.id,
category: b.category || 'Technology',
title: b.title,
excerpt: b.excerpt || '',
content: b.content || '',
readTime: b.read_time || '5 min read',
date: dateStr,
image,
slug: b.slug,
author,
authorDesignation: authorRole,
editor,
editorDesignation: editorRole,
tags: b.tags ? JSON.parse(b.tags) : []
});
});
return blogs.length > 0 ? blogs : null;
} catch (err) {
console.warn('Note: Strapi SQLite DB sync check bypassed:', err.message);
return null;
}
}