diff --git a/Channel-Backend/prisma/schema.prisma b/Channel-Backend/prisma/schema.prisma index e4764d6..ce3b888 100644 --- a/Channel-Backend/prisma/schema.prisma +++ b/Channel-Backend/prisma/schema.prisma @@ -45,6 +45,7 @@ model User { companySize String? defaultTheme String? @default("dark") companyName String? + showEcosystemTab Boolean @default(true) assignedNda LegalDocument? @relation("AssignedNda", fields: [assignedNdaId], references: [id], onDelete: SetNull) assignedMsa LegalDocument? @relation("AssignedMsa", fields: [assignedMsaId], references: [id], onDelete: SetNull) organization Organization? @relation(fields: [organizationId], references: [id]) @@ -163,3 +164,22 @@ model AssetGroup { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model EcosystemOffering { + id String @id @default(uuid()) + name String @unique + type String // "PRODUCT" | "SERVICE" + tagline String + description String @db.Text + benefits String[] + websiteUrl String + ctaText String @default("Visit Website") + logoIcon String @default("Globe") + logoUrl String? + mediaUrl String? + mediaType String? // "IMAGE" | "VIDEO" | "GIF" + orderIndex Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/Channel-Backend/seed.ts b/Channel-Backend/seed.ts index 6491305..a58638c 100644 --- a/Channel-Backend/seed.ts +++ b/Channel-Backend/seed.ts @@ -113,6 +113,90 @@ async function seed() { } else { console.log(`Partner (Approved) already exists: ${activeEmail}`); } + + // 5. Seed Ecosystem Offerings + const offerings = [ + { + name: 'CodeNuk', + type: 'PRODUCT', + tagline: 'Accelerate software delivery with deterministic AI-powered backend generation.', + description: 'CodeNuk is an enterprise-grade AI platform that transforms Software Requirements Specifications (SRS) into production-ready backend foundations. By deterministically generating architecture, database schemas, APIs, security components, testing frameworks, and deployment-ready assets directly from business requirements, CodeNuk eliminates weeks of repetitive engineering effort while ensuring consistency, traceability, and architectural standardization.', + benefits: [ + 'Accelerates software development and time-to-market.', + 'Standardizes backend architecture across projects.', + 'Minimizes manual engineering effort and setup time.', + 'Improves code consistency and traceability to business requirements.', + 'Delivers deployment-ready backend assets with built-in testing and documentation.', + 'Commercial model based on one-time project pricing instead of recurring subscriptions.' + ], + websiteUrl: 'https://codenuk.com', + ctaText: 'Visit CodeNuk', + logoIcon: 'Code', + logoUrl: 'codenuk', + orderIndex: 0 + }, + { + name: 'Tech4Biz Solutions', + type: 'SERVICE', + tagline: 'A strategic Technology Execution Partner delivering end-to-end digital engineering and transformation services.', + description: 'Tech4Biz Solutions partners with enterprises to architect, build, integrate, and scale secure, future-ready technology solutions. Our expertise spans software engineering, AI, cloud, automation, IoT, and cybersecurity, enabling organizations to accelerate digital transformation, modernize legacy systems, and deliver technology initiatives with speed, quality, and confidence.', + benefits: [ + 'Custom Software Engineering & Cloud Solutions', + 'AI, Automation & IoT Integration', + 'Legacy Modernization & Cybersecurity Audit', + 'Strategic Technology Execution & Ownership' + ], + websiteUrl: 'https://www.tech4bizsolutions.com', + ctaText: 'Visit Tech4Biz', + logoIcon: 'Briefcase', + logoUrl: 'tech4biz', + orderIndex: 1 + }, + { + name: 'Audittrax Labs', + type: 'SERVICE', + tagline: 'An AI-powered platform delivering Tech Due Diligence, Technical Advisory, and Continuous Assurance.', + description: 'Audittrax Labs enables enterprises, investors, and business leaders to make informed technology decisions through Tech Due Diligence, Technical Advisory, and AI-driven audit, risk, and compliance services. By combining continuous assurance, automated controls monitoring, and expert technical assessments, the platform helps organizations evaluate technology landscapes, mitigate risk, strengthen governance, and accelerate confident business decisions.', + benefits: [ + 'AI-Driven Compliance & Tech Auditing', + 'Comprehensive Tech Due Diligence for Investors', + 'Automated Security & Risk Monitoring', + 'Technical Advisory & Governance Solutions' + ], + websiteUrl: 'https://auditraxlabs.com', + ctaText: 'Visit Audittrax', + logoIcon: 'Shield', + logoUrl: 'auditraxlabs', + orderIndex: 2 + }, + { + name: 'Cloudtopiaa', + type: 'PRODUCT', + tagline: 'An enterprise cloud platform delivering secure, scalable, and high-performance infrastructure services.', + description: 'Cloudtopiaa enables organizations to accelerate their cloud journey through enterprise-grade infrastructure, storage, networking, security, and cloud-native services. Designed for modern workloads, the platform helps businesses migrate, deploy, manage, and scale applications with improved resilience, operational efficiency, and cost optimization.', + benefits: [ + 'Enterprise-grade secure infrastructure', + 'High-performance storage, networking, and cloud-native services', + 'Cost optimization & operational efficiency audit', + 'Seamless cloud migration and automation tools' + ], + websiteUrl: 'https://cloudtopiaa.com', + ctaText: 'Visit Cloudtopiaa', + logoIcon: 'Cloud', + logoUrl: 'cloudtopiaa', + orderIndex: 3 + } + ]; + + for (const offering of offerings) { + await prisma.ecosystemOffering.upsert({ + where: { name: offering.name }, + update: offering, + create: offering + }); + } + console.log('Seeded Ecosystem Offerings.'); + console.log('Seeding completed successfully.'); } diff --git a/Channel-Backend/src/app.ts b/Channel-Backend/src/app.ts index 8396494..d58df79 100644 --- a/Channel-Backend/src/app.ts +++ b/Channel-Backend/src/app.ts @@ -15,6 +15,7 @@ import authRoutes from './routes/auth.routes'; import assetRoutes from './routes/asset.routes'; import orgRoutes from './routes/organization.routes'; import legalRoutes from './routes/legal.routes'; +import ecosystemRoutes from './routes/ecosystem.routes'; import { ensureBucketExists } from './utils/s3'; import { originStorage } from './utils/origin-storage'; @@ -107,6 +108,7 @@ app.use('/api/v1/auth', authRoutes); app.use('/api/v1/assets', assetRoutes); app.use('/api/v1/organizations', orgRoutes); app.use('/api/v1/legal', legalRoutes); +app.use('/api/v1/ecosystem', ecosystemRoutes); app.get('/api/v1/health', (req: Request, res: Response) => { res.status(200).json({ status: 'success', message: 'API is fully functional and real.' }); diff --git a/Channel-Backend/src/controllers/auth.controller.ts b/Channel-Backend/src/controllers/auth.controller.ts index 4a5eb9d..256464e 100644 --- a/Channel-Backend/src/controllers/auth.controller.ts +++ b/Channel-Backend/src/controllers/auth.controller.ts @@ -25,7 +25,7 @@ export class AuthController { public invitePartner = async (req: Request, res: Response, next: NextFunction) => { try { - const { email, organizationId, partnerGroup, assignedNdaId, assignedMsaId, sharedAssetIds, mfaEnabled } = z.object({ + const { email, organizationId, partnerGroup, assignedNdaId, assignedMsaId, sharedAssetIds, mfaEnabled, showEcosystemTab } = z.object({ email: z.string().email(), organizationId: z.string().uuid().optional(), partnerGroup: z.string().optional().nullable(), @@ -33,6 +33,7 @@ export class AuthController { assignedMsaId: z.string().optional().nullable(), sharedAssetIds: z.array(z.string().uuid()).optional(), mfaEnabled: z.boolean().optional(), + showEcosystemTab: z.boolean().optional(), }).parse(req.body); const result = await this.authService.invitePartner(email, { @@ -42,6 +43,7 @@ export class AuthController { assignedMsaId: assignedMsaId || undefined, sharedAssetIds, mfaEnabled, + showEcosystemTab, }); res.status(201).json({ message: 'Invite created', token: result.inviteToken }); } catch(err) { next(err); } @@ -50,12 +52,13 @@ export class AuthController { public updatePartner = async (req: Request, res: Response, next: NextFunction) => { try { const { partnerId } = req.params; - const { partnerGroup, assignedNdaId, assignedMsaId, sharedAssetIds, mfaEnabled } = z.object({ + const { partnerGroup, assignedNdaId, assignedMsaId, sharedAssetIds, mfaEnabled, showEcosystemTab } = z.object({ partnerGroup: z.string().optional().nullable(), assignedNdaId: z.string().optional().nullable(), assignedMsaId: z.string().optional().nullable(), sharedAssetIds: z.array(z.string().uuid()).optional(), mfaEnabled: z.boolean().optional(), + showEcosystemTab: z.boolean().optional(), }).parse(req.body); const result = await this.authService.updatePartner(partnerId, { @@ -64,6 +67,7 @@ export class AuthController { assignedMsaId: assignedMsaId === undefined ? undefined : assignedMsaId, sharedAssetIds, mfaEnabled, + showEcosystemTab, }); res.status(200).json(result); } catch(err) { next(err); } diff --git a/Channel-Backend/src/controllers/ecosystem.controller.ts b/Channel-Backend/src/controllers/ecosystem.controller.ts new file mode 100644 index 0000000..d4dffbc --- /dev/null +++ b/Channel-Backend/src/controllers/ecosystem.controller.ts @@ -0,0 +1,114 @@ +import { Request, Response, NextFunction } from 'express'; +import path from 'path'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { s3Client, BUCKET_NAME } from '../utils/s3'; +import prisma from '../utils/db'; + +export class EcosystemController { + public listOfferings = async (req: Request, res: Response, next: NextFunction) => { + try { + const user = (req as any).user; + const isAdmin = user?.role === 'ADMIN'; + + const offerings = await prisma.ecosystemOffering.findMany({ + where: isAdmin ? undefined : { isActive: true }, + orderBy: { orderIndex: 'asc' } + }); + + res.status(200).json(offerings); + } catch (err) { + next(err); + } + }; + + public createOffering = async (req: Request, res: Response, next: NextFunction) => { + try { + const data = req.body; + const created = await prisma.ecosystemOffering.create({ + data: { + name: data.name, + type: data.type, + tagline: data.tagline, + description: data.description, + benefits: data.benefits || [], + websiteUrl: data.websiteUrl, + ctaText: data.ctaText || 'Visit Website', + logoIcon: data.logoIcon || 'Globe', + logoUrl: data.logoUrl || null, + mediaUrl: data.mediaUrl || null, + mediaType: data.mediaType || null, + orderIndex: data.orderIndex !== undefined ? data.orderIndex : 0, + isActive: data.isActive !== undefined ? data.isActive : true, + } + }); + res.status(201).json(created); + } catch (err) { + next(err); + } + }; + + public updateOffering = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const data = req.body; + + const updated = await prisma.ecosystemOffering.update({ + where: { id }, + data: { + name: data.name, + type: data.type, + tagline: data.tagline, + description: data.description, + benefits: data.benefits, + websiteUrl: data.websiteUrl, + ctaText: data.ctaText, + logoIcon: data.logoIcon, + logoUrl: data.logoUrl, + mediaUrl: data.mediaUrl, + mediaType: data.mediaType, + orderIndex: data.orderIndex, + isActive: data.isActive, + } + }); + + res.status(200).json(updated); + } catch (err) { + next(err); + } + }; + + public deleteOffering = async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + await prisma.ecosystemOffering.delete({ + where: { id } + }); + res.status(200).json({ message: 'Offering deleted successfully' }); + } catch (err) { + next(err); + } + }; + + public uploadFile = async (req: Request, res: Response, next: NextFunction) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const filename = uniqueSuffix + path.extname(req.file.originalname); + + await s3Client.send(new PutObjectCommand({ + Bucket: BUCKET_NAME, + Key: filename, + Body: req.file.buffer, + ContentType: req.file.mimetype, + })); + + const fileUrl = `/uploads/${filename}`; + res.status(200).json({ url: fileUrl }); + } catch (err) { + next(err); + } + }; +} diff --git a/Channel-Backend/src/routes/ecosystem.routes.ts b/Channel-Backend/src/routes/ecosystem.routes.ts new file mode 100644 index 0000000..7a2cd95 --- /dev/null +++ b/Channel-Backend/src/routes/ecosystem.routes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { EcosystemController } from '../controllers/ecosystem.controller'; +import { authenticate, requireRole } from '../middleware/auth.middleware'; +import { upload } from '../middleware/upload.middleware'; + +const router = Router(); +const controller = new EcosystemController(); + +router.get('/offerings', authenticate, controller.listOfferings); +router.post('/offerings', authenticate, requireRole('ADMIN'), controller.createOffering); +router.put('/offerings/:id', authenticate, requireRole('ADMIN'), controller.updateOffering); +router.delete('/offerings/:id', authenticate, requireRole('ADMIN'), controller.deleteOffering); +router.post('/upload', authenticate, requireRole('ADMIN'), upload.single('file'), controller.uploadFile); + +export default router; diff --git a/Channel-Backend/src/services/auth.service.ts b/Channel-Backend/src/services/auth.service.ts index 41625a2..1150c9c 100644 --- a/Channel-Backend/src/services/auth.service.ts +++ b/Channel-Backend/src/services/auth.service.ts @@ -50,7 +50,7 @@ export class AuthService { return this.getUserById(user.id); } - public async invitePartner(email: string, options: { organizationId?: string, partnerGroup?: string, assignedNdaId?: string, assignedMsaId?: string, sharedAssetIds?: string[], mfaEnabled?: boolean } = {}) { + public async invitePartner(email: string, options: { organizationId?: string, partnerGroup?: string, assignedNdaId?: string, assignedMsaId?: string, sharedAssetIds?: string[], mfaEnabled?: boolean, showEcosystemTab?: boolean } = {}) { const existing = await prisma.user.findUnique({ where: { email } }); if (existing) throw new AppError('Email already in use', 400); @@ -103,6 +103,7 @@ export class AuthService { inviteTokenExp, onboardingStatus, mfaEnabled: options.mfaEnabled !== undefined ? options.mfaEnabled : true, + showEcosystemTab: options.showEcosystemTab !== undefined ? options.showEcosystemTab : true, partnerGroup: options.partnerGroup || null, assignedNdaId, assignedMsaId, @@ -137,7 +138,7 @@ export class AuthService { return { inviteToken, emailSent, emailError }; } - public async updatePartner(partnerId: string, options: { partnerGroup?: string | null, assignedNdaId?: string | null, assignedMsaId?: string | null, sharedAssetIds?: string[], mfaEnabled?: boolean }) { + public async updatePartner(partnerId: string, options: { partnerGroup?: string | null, assignedNdaId?: string | null, assignedMsaId?: string | null, sharedAssetIds?: string[], mfaEnabled?: boolean, showEcosystemTab?: boolean }) { const user = await prisma.user.findUnique({ where: { id: partnerId } }); @@ -184,6 +185,7 @@ export class AuthService { assignedNdaId, assignedMsaId, mfaEnabled: options.mfaEnabled !== undefined ? options.mfaEnabled : user.mfaEnabled, + showEcosystemTab: options.showEcosystemTab !== undefined ? options.showEcosystemTab : user.showEcosystemTab, } }); @@ -377,6 +379,7 @@ export class AuthService { email: true, onboardingStatus: true, mfaEnabled: true, + showEcosystemTab: true, createdAt: true, updatedAt: true, organizationId: true, @@ -451,6 +454,7 @@ export class AuthService { updatedAt: true, assignedNdaId: true, assignedMsaId: true, + showEcosystemTab: true, organization: { select: { id: true, @@ -488,6 +492,7 @@ export class AuthService { updatedAt: true, assignedNdaId: true, assignedMsaId: true, + showEcosystemTab: true, organization: { select: { id: true, diff --git a/Channel-Frontend/src/app/layouts/AdminLayout.tsx b/Channel-Frontend/src/app/layouts/AdminLayout.tsx index 0b919c7..20f450e 100644 --- a/Channel-Frontend/src/app/layouts/AdminLayout.tsx +++ b/Channel-Frontend/src/app/layouts/AdminLayout.tsx @@ -14,6 +14,7 @@ import { Moon, ChevronRight, ChevronLeft, + Globe } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; @@ -35,6 +36,7 @@ export const AdminLayout: React.FC = () => { { name: "Approvals Queue", path: "/admin/approvals", icon: ClipboardCheck }, { name: "Legal Templates", path: "/admin/legal", icon: ShieldCheck }, { name: "Manage Catalog", path: "/admin/assets", icon: FolderGit2 }, + { name: "Ecosystem Manager", path: "/admin/ecosystem", icon: Globe }, ]; return ( diff --git a/Channel-Frontend/src/app/layouts/ClientLayout.tsx b/Channel-Frontend/src/app/layouts/ClientLayout.tsx index 65256a7..a328ad5 100644 --- a/Channel-Frontend/src/app/layouts/ClientLayout.tsx +++ b/Channel-Frontend/src/app/layouts/ClientLayout.tsx @@ -2,13 +2,9 @@ import React, { useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useThemeStore } from '../../hooks/use-theme'; import { useAuthStore } from '../../hooks/use-auth'; -import { Cpu, LogOut, CheckCircle, Clock, Menu, X, Sun, Moon, ChevronRight, ChevronLeft, Settings } from 'lucide-react'; +import { Cpu, LogOut, CheckCircle, Clock, Menu, X, Sun, Moon, ChevronRight, ChevronLeft, Settings, Globe } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; -const navItems = [ - { name: 'Assets', path: '/client', icon: Cpu, label: 'Asset Explorer' }, -]; - export const ClientLayout: React.FC = () => { const { user, logout } = useAuthStore(); const { theme, toggleTheme } = useThemeStore(); @@ -17,6 +13,11 @@ export const ClientLayout: React.FC = () => { const [mobileOpen, setMobileOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); + const dynamicNavItems = [ + { name: 'Assets', path: '/client', icon: Cpu, label: 'Asset Explorer' }, + { name: 'Ecosystem', path: '/client/ecosystem', icon: Globe, label: 'Explore More' } + ]; + // Settings Modal State const [settingsOpen, setSettingsOpen] = useState(false); const [defaultTheme, setDefaultTheme] = useState(user?.defaultTheme || 'dark'); @@ -115,7 +116,7 @@ export const ClientLayout: React.FC = () => { {/* Navigation */}