Compare commits
No commits in common. "8bb20a99aaacdf5f91ee08be5718dfd8fa19dd43" and "a87fd01ec82954209c4fbcb515c956221daeddc2" have entirely different histories.
8bb20a99aa
...
a87fd01ec8
@ -28,7 +28,7 @@ app.use(helmet({
|
|||||||
useDefaults: false,
|
useDefaults: false,
|
||||||
directives: {
|
directives: {
|
||||||
"default-src": helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc,
|
"default-src": helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc,
|
||||||
"frame-ancestors": ["'self'", "http://localhost:5173", "http://localhost:5000", "https://*.ngrok-free.dev", "https://*.ngrok.io"],
|
"frame-ancestors": ["'self'", "http://localhost:5173", "http://localhost:5000"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
frameguard: false,
|
frameguard: false,
|
||||||
|
|||||||
@ -88,20 +88,6 @@ export class AssetController {
|
|||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public bulkShareAssets = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const { assetIds, shares } = req.body;
|
|
||||||
if (!Array.isArray(assetIds)) {
|
|
||||||
return res.status(400).json({ error: 'assetIds must be an array' });
|
|
||||||
}
|
|
||||||
if (!Array.isArray(shares)) {
|
|
||||||
return res.status(400).json({ error: 'shares must be an array' });
|
|
||||||
}
|
|
||||||
await this.assetService.bulkShareAssets(assetIds, shares);
|
|
||||||
res.status(200).json({ message: 'Assets shared successfully' });
|
|
||||||
} catch (err) { next(err); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public shareAsset = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
public shareAsset = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { organizationIds } = req.body;
|
const { organizationIds } = req.body;
|
||||||
|
|||||||
@ -10,7 +10,6 @@ router.use(authenticate);
|
|||||||
|
|
||||||
router.post('/upload', requireRole('ADMIN'), upload.single('file'), assetController.uploadAsset);
|
router.post('/upload', requireRole('ADMIN'), upload.single('file'), assetController.uploadAsset);
|
||||||
router.get('/', assetController.listAssets);
|
router.get('/', assetController.listAssets);
|
||||||
router.patch('/bulk-share', requireRole('ADMIN'), assetController.bulkShareAssets);
|
|
||||||
router.get('/:id', assetController.getAsset);
|
router.get('/:id', assetController.getAsset);
|
||||||
router.patch('/:id', requireRole('ADMIN'), assetController.updateAsset);
|
router.patch('/:id', requireRole('ADMIN'), assetController.updateAsset);
|
||||||
router.post('/:id/share', requireRole('ADMIN'), assetController.shareAsset);
|
router.post('/:id/share', requireRole('ADMIN'), assetController.shareAsset);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ async function migrateLocalUploads() {
|
|||||||
console.log('[Migration] Starting local uploads migration to MinIO...');
|
console.log('[Migration] Starting local uploads migration to MinIO...');
|
||||||
await ensureBucketExists();
|
await ensureBucketExists();
|
||||||
|
|
||||||
const localUploadsDir = path.join(__dirname, '../../minio-seed');
|
const localUploadsDir = path.join(__dirname, '../../uploads');
|
||||||
if (!fs.existsSync(localUploadsDir)) {
|
if (!fs.existsSync(localUploadsDir)) {
|
||||||
console.log('[Migration] No local uploads directory found.');
|
console.log('[Migration] No local uploads directory found.');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -267,24 +267,6 @@ export class AssetService {
|
|||||||
return this.getAssetById(assetId);
|
return this.getAssetById(assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkShareAssets(assetIds: string[], shares: any[]) {
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.sharedAsset.deleteMany({
|
|
||||||
where: { assetId: { in: assetIds } }
|
|
||||||
}),
|
|
||||||
prisma.sharedAsset.createMany({
|
|
||||||
data: assetIds.flatMap(assetId =>
|
|
||||||
shares.map(s => ({
|
|
||||||
assetId,
|
|
||||||
organizationId: s.organizationId,
|
|
||||||
userId: s.userId || null,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
skipDuplicates: true
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async incrementDownloadCount(id: string) {
|
public async incrementDownloadCount(id: string) {
|
||||||
return await prisma.asset.update({
|
return await prisma.asset.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
@ -5,6 +5,10 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2375BF46' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z' /%3E%3C/svg%3E" />
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2375BF46' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z' /%3E%3C/svg%3E" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tech4Biz Client & Admin Portal</title>
|
<title>Tech4Biz Client & Admin Portal</title>
|
||||||
|
<!-- Premium Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
10
Channel-Frontend/package-lock.json
generated
10
Channel-Frontend/package-lock.json
generated
@ -8,7 +8,6 @@
|
|||||||
"name": "tech4biz-channel",
|
"name": "tech4biz-channel",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@tanstack/react-router": "^1.170.17",
|
"@tanstack/react-router": "^1.170.17",
|
||||||
@ -70,15 +69,6 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fontsource-variable/inter": {
|
|
||||||
"version": "5.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
|
|
||||||
"integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
|
|
||||||
"license": "OFL-1.1",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hookform/resolvers": {
|
"node_modules/@hookform/resolvers": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.4.0",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.2",
|
||||||
"@tanstack/react-router": "^1.170.17",
|
"@tanstack/react-router": "^1.170.17",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 266 KiB |
@ -63,8 +63,8 @@ export const AdminLayout: React.FC = () => {
|
|||||||
className={`h-24 flex items-center border-b border-ink-200 transition-all duration-300 ${isCollapsed ? "justify-center px-4" : "px-8"}`}
|
className={`h-24 flex items-center border-b border-ink-200 transition-all duration-300 ${isCollapsed ? "justify-center px-4" : "px-8"}`}
|
||||||
>
|
>
|
||||||
<Link to="/admin" className="flex items-center gap-3 shrink-0 group">
|
<Link to="/admin" className="flex items-center gap-3 shrink-0 group">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-ink-0 shadow-sm border border-ink-200 group-hover:scale-105 transition-all duration-300 p-1">
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-gradient-to-br from-ink-900 to-ink-800 shadow-lg group-hover:scale-105 transition-all duration-300">
|
||||||
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
|
<ShieldCheck className="w-5 h-5 text-ink-0" />
|
||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="flex flex-col animate-fade-in">
|
<div className="flex flex-col animate-fade-in">
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export const ClientLayout: React.FC = () => {
|
|||||||
{/* Branding */}
|
{/* Branding */}
|
||||||
<div className={`h-24 flex items-center border-b border-ink-200 transition-all duration-300 ${isCollapsed ? 'justify-center px-4' : 'px-8'}`}>
|
<div className={`h-24 flex items-center border-b border-ink-200 transition-all duration-300 ${isCollapsed ? 'justify-center px-4' : 'px-8'}`}>
|
||||||
<Link to="/client" className="flex items-center gap-3 shrink-0 group">
|
<Link to="/client" className="flex items-center gap-3 shrink-0 group">
|
||||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-ink-0 shadow-sm border border-ink-200 group-hover:scale-105 transition-all duration-300 p-1">
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center bg-gradient-to-br from-ink-900 to-ink-800 shadow-lg group-hover:scale-105 transition-all duration-300">
|
||||||
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
|
<ShieldCheck className="w-5 h-5 text-ink-0" />
|
||||||
</div>
|
</div>
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<div className="flex flex-col animate-fade-in">
|
<div className="flex flex-col animate-fade-in">
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Outlet, Link } from '@tanstack/react-router';
|
|||||||
import { useAuthStore } from '../../hooks/use-auth';
|
import { useAuthStore } from '../../hooks/use-auth';
|
||||||
import { useThemeStore } from '../../hooks/use-theme';
|
import { useThemeStore } from '../../hooks/use-theme';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { LayoutDashboard, FolderKanban, FileSignature, Users, LogOut, Sun, Moon } from 'lucide-react';
|
import { LayoutDashboard, FolderKanban, FileSignature, Users, LogOut, Hexagon, Sun, Moon } from 'lucide-react';
|
||||||
|
|
||||||
export const MainLayout = () => {
|
export const MainLayout = () => {
|
||||||
const { isAuthenticated, logout, user } = useAuthStore();
|
const { isAuthenticated, logout, user } = useAuthStore();
|
||||||
@ -29,8 +29,8 @@ export const MainLayout = () => {
|
|||||||
>
|
>
|
||||||
<div className="h-24 flex items-center px-8 border-b border-ink-200">
|
<div className="h-24 flex items-center px-8 border-b border-ink-200">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="relative flex items-center justify-center w-11 h-11 rounded-2xl bg-ink-0 border border-ink-200 shadow-sm p-1">
|
<div className="relative flex items-center justify-center w-11 h-11 rounded-2xl bg-gradient-to-tr from-ink-900 to-ink-800 shadow-lg">
|
||||||
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
|
<Hexagon className="text-ink-0 w-6 h-6 absolute" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-extrabold text-xl tracking-tight text-ink-900">Tech4Biz</span>
|
<span className="font-extrabold text-xl tracking-tight text-ink-900">Tech4Biz</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -210,7 +210,7 @@ export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fileHost = (
|
const fileHost = (
|
||||||
import.meta.env.VITE_API_URL || "/api/v1"
|
import.meta.env.VITE_API_URL || "http://localhost:5000/api/v1"
|
||||||
).replace("/api/v1", "");
|
).replace("/api/v1", "");
|
||||||
const isBothVerified = verifiedDocs.nda && verifiedDocs.msa;
|
const isBothVerified = verifiedDocs.nda && verifiedDocs.msa;
|
||||||
const isCurrentVerified =
|
const isCurrentVerified =
|
||||||
|
|||||||
@ -12,13 +12,13 @@ export const PageHeader: React.FC<PageHeaderProps> = ({ title, subtitle, badge,
|
|||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-ink-200">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pb-4 border-b border-ink-200">
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
<div className="flex flex-col gap-1 min-w-0">
|
||||||
<div className="flex items-center gap-2.5 flex-wrap">
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
<h1 className="text-xl page-title font-bold tracking-tight text-ink-900 truncate">
|
<h1 className="text-xl font-bold tracking-tight text-ink-900 truncate">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{badge && <div className="shrink-0">{badge}</div>}
|
{badge && <div className="shrink-0">{badge}</div>}
|
||||||
</div>
|
</div>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-xs page-subtitle font-medium text-ink-500 truncate max-w-3xl">
|
<p className="text-xs font-medium text-ink-500 truncate max-w-3xl">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Asset } from '../../../types/assets';
|
import type { Asset } from '../../../types/assets';
|
||||||
import type { User } from '../../../types/auth';
|
import type { User } from '../../../types/auth';
|
||||||
import { axiosInstance } from '../../../services/axios';
|
|
||||||
|
|
||||||
interface AssetCardProps {
|
interface AssetCardProps {
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
@ -31,8 +30,6 @@ interface AssetCardProps {
|
|||||||
onOpenViewer: (asset: Asset) => void;
|
onOpenViewer: (asset: Asset) => void;
|
||||||
onDownload: (asset: Asset) => void;
|
onDownload: (asset: Asset) => void;
|
||||||
onRequestDownload: (asset: Asset) => void;
|
onRequestDownload: (asset: Asset) => void;
|
||||||
isSelected?: boolean;
|
|
||||||
onToggleSelect?: (assetId: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AssetCard: React.FC<AssetCardProps> = ({
|
export const AssetCard: React.FC<AssetCardProps> = ({
|
||||||
@ -46,9 +43,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onOpenViewer,
|
onOpenViewer,
|
||||||
onDownload,
|
onDownload,
|
||||||
onRequestDownload,
|
onRequestDownload
|
||||||
isSelected = false,
|
|
||||||
onToggleSelect
|
|
||||||
}) => {
|
}) => {
|
||||||
const isMenuOpen = activeMenuId === asset.id;
|
const isMenuOpen = activeMenuId === asset.id;
|
||||||
const canDirectDownload = user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED';
|
const canDirectDownload = user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED';
|
||||||
@ -70,229 +65,92 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
return File;
|
return File;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRenderable = (type: string, url: string) => {
|
const isRenderable = (type: string) => {
|
||||||
const isOffice = type.includes('word') || type.includes('presentation') || type.includes('sheet') ||
|
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg');
|
||||||
url.toLowerCase().endsWith('.docx') || url.toLowerCase().endsWith('.doc') ||
|
|
||||||
url.toLowerCase().endsWith('.pptx') || url.toLowerCase().endsWith('.ppt') ||
|
|
||||||
url.toLowerCase().endsWith('.xlsx') || url.toLowerCase().endsWith('.xls');
|
|
||||||
return type === 'url' || type.includes('pdf') || type.includes('image') || type.includes('png') || type.includes('jpg') || isOffice;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFullAssetUrl = (url: string) => {
|
|
||||||
let resolvedUrl = url;
|
|
||||||
if (!url.startsWith('http')) {
|
|
||||||
const backendBase = axiosInstance.defaults.baseURL || '/api/v1';
|
|
||||||
const relativeHost = backendBase.replace('/api/v1', '');
|
|
||||||
resolvedUrl = relativeHost.startsWith('/') || relativeHost === ''
|
|
||||||
? `${window.location.origin}${relativeHost}${url}`
|
|
||||||
: `${relativeHost}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentOrigin = window.location.origin;
|
|
||||||
const isCurrentOriginLocal = currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1');
|
|
||||||
if (!isCurrentOriginLocal && (resolvedUrl.includes('localhost') || resolvedUrl.includes('127.0.0.1'))) {
|
|
||||||
resolvedUrl = resolvedUrl.replace(/https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/, currentOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Icon = getAssetIcon(asset.type);
|
const Icon = getAssetIcon(asset.type);
|
||||||
|
|
||||||
const isImage = asset.type.includes('image') || asset.type.includes('png') || asset.type.includes('jpg') || asset.url.match(/\.(png|jpe?g|gif|svg|webp)$/i);
|
|
||||||
const isPdf = asset.type.includes('pdf') || asset.url.toLowerCase().endsWith('.pdf');
|
|
||||||
const isWord = asset.type.includes('word') || asset.url.toLowerCase().endsWith('.docx') || asset.url.toLowerCase().endsWith('.doc');
|
|
||||||
const isPresentation = asset.type.includes('presentation') || asset.url.toLowerCase().endsWith('.pptx') || asset.url.toLowerCase().endsWith('.ppt');
|
|
||||||
const isSpreadsheet = asset.type.includes('sheet') || asset.url.toLowerCase().endsWith('.xlsx') || asset.url.toLowerCase().endsWith('.xls') || asset.url.toLowerCase().endsWith('.csv');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`group relative bg-ink-0 border rounded-xl p-4 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex flex-col justify-between ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : 'border-ink-200 hover:border-ink-300'}`}>
|
<div className="group relative bg-ink-0 border border-ink-200 rounded-xl p-4 hover:border-ink-300 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 flex flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
{/* Visual Thumbnail Area */}
|
<div className="flex justify-between items-start mb-3 relative">
|
||||||
<div className="w-full h-36 bg-ink-50 border border-ink-200 rounded-lg mb-3 flex items-center justify-center overflow-hidden relative select-none group-hover:border-ink-300 transition-colors bg-gradient-to-br from-ink-50 to-ink-100/50">
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center text-ink-900 bg-ink-100 border border-ink-200">
|
||||||
{/* Absolute overlays for controls */}
|
<Icon className="w-5 h-5" />
|
||||||
<div className="absolute top-2 left-2 z-10 flex items-center gap-1.5">
|
|
||||||
{user?.role === 'ADMIN' && onToggleSelect && (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => onToggleSelect(asset.id)}
|
|
||||||
className="w-3.5 h-3.5 rounded border-ink-300 bg-ink-0 text-ink-900 focus:ring-ink-900/10 cursor-pointer shadow-sm"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute top-2 right-2 z-10 flex items-center gap-1">
|
<div className="relative flex items-center gap-1.5">
|
||||||
{isRenderable(asset.type, asset.url) && (
|
{isRenderable(asset.type) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onOpenViewer(asset)}
|
onClick={() => onOpenViewer(asset)}
|
||||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
className="p-1 rounded-lg text-ink-500 hover:text-ink-900 hover:bg-ink-100 transition-colors"
|
||||||
title="Preview Online"
|
title="Preview Online"
|
||||||
>
|
>
|
||||||
<Eye className="w-3.5 h-3.5" />
|
<Eye className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative">
|
<button
|
||||||
<button
|
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
||||||
onClick={() => setActiveMenuId(isMenuOpen ? null : asset.id)}
|
className="p-1 rounded-lg text-ink-400 hover:text-ink-900 hover:bg-ink-100 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||||
className="p-1 rounded-md bg-ink-0/90 backdrop-blur text-ink-600 hover:text-ink-900 border border-ink-200 shadow-sm transition-all hover:scale-105"
|
>
|
||||||
>
|
<MoreVertical className="w-4 h-4" />
|
||||||
<MoreVertical className="w-3.5 h-3.5" />
|
</button>
|
||||||
</button>
|
|
||||||
|
{isMenuOpen && (
|
||||||
{isMenuOpen && (
|
<>
|
||||||
<>
|
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
||||||
<div className="fixed inset-0 z-10" onClick={() => setActiveMenuId(null)} />
|
<div className="absolute right-0 mt-8 w-44 bg-ink-0 border border-ink-200 rounded-lg shadow-lg z-20 overflow-hidden py-1">
|
||||||
<div className="absolute right-0 mt-1.5 w-44 bg-ink-0 border border-ink-200 rounded-lg shadow-lg z-20 overflow-hidden py-1">
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() => {
|
onViewDetails(asset);
|
||||||
onViewDetails(asset);
|
setActiveMenuId(null);
|
||||||
setActiveMenuId(null);
|
}}
|
||||||
}}
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
||||||
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
>
|
||||||
>
|
<File className="w-3.5 h-3.5" />
|
||||||
<File className="w-3.5 h-3.5" />
|
View Details
|
||||||
View Details
|
</button>
|
||||||
</button>
|
{user?.role === 'ADMIN' && (
|
||||||
{user?.role === 'ADMIN' && (
|
<>
|
||||||
<>
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() => {
|
onEdit(asset);
|
||||||
onEdit(asset);
|
setActiveMenuId(null);
|
||||||
setActiveMenuId(null);
|
}}
|
||||||
}}
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
||||||
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
>
|
||||||
>
|
<Edit3 className="w-3.5 h-3.5" />
|
||||||
<Edit3 className="w-3.5 h-3.5" />
|
Edit Asset
|
||||||
Edit Asset
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() => {
|
onShare(asset);
|
||||||
onShare(asset);
|
setActiveMenuId(null);
|
||||||
setActiveMenuId(null);
|
}}
|
||||||
}}
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
||||||
className="w-full text-left px-4 py-2 text-xs font-semibold text-ink-700 hover:bg-ink-50 hover:text-ink-900 flex items-center gap-2"
|
>
|
||||||
>
|
<Share2 className="w-3.5 h-3.5" />
|
||||||
<Share2 className="w-3.5 h-3.5" />
|
Share Settings
|
||||||
Share Settings
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() => {
|
onDelete(asset.id);
|
||||||
onDelete(asset.id);
|
setActiveMenuId(null);
|
||||||
setActiveMenuId(null);
|
}}
|
||||||
}}
|
className="w-full text-left px-4 py-2 text-xs font-semibold text-red-650 hover:bg-red-500/10 flex items-center gap-2"
|
||||||
className="w-full text-left px-4 py-2 text-xs font-semibold text-red-650 hover:bg-red-500/10 flex items-center gap-2"
|
>
|
||||||
>
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
Delete Asset
|
||||||
Delete Asset
|
</button>
|
||||||
</button>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thumbnail Preview Area Content */}
|
|
||||||
{isImage ? (
|
|
||||||
<img
|
|
||||||
src={getFullAssetUrl(asset.url)}
|
|
||||||
alt={asset.title}
|
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
const placeholder = document.getElementById(`fallback-${asset.id}`);
|
|
||||||
if (placeholder) placeholder.style.display = 'flex';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Fallbacks / simulated thumbnails */}
|
|
||||||
<div
|
|
||||||
id={`fallback-${asset.id}`}
|
|
||||||
className="w-full h-full flex items-center justify-center"
|
|
||||||
style={{ display: isImage ? 'none' : 'flex' }}
|
|
||||||
>
|
|
||||||
{isPdf ? (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4 relative">
|
|
||||||
<div className="w-14 h-18 bg-ink-0 border border-ink-200 shadow-sm rounded flex flex-col justify-between p-1.5 relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 left-0 bg-red-600 text-ink-0 py-0.5 text-[8px] font-extrabold uppercase text-center tracking-wider">
|
|
||||||
PDF
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 mt-4">
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-5/6" />
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-full" />
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-4/5" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-0.5 mt-1">
|
|
||||||
<span>PDF</span>
|
|
||||||
<FileText className="w-2.5 h-2.5 text-red-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
) : isPresentation ? (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4">
|
|
||||||
<div className="w-18 h-13 bg-amber-500 border border-amber-600 shadow-sm rounded-lg flex flex-col justify-between p-1.5 relative overflow-hidden">
|
|
||||||
<div className="h-1.5 bg-ink-0/90 rounded w-3/4 mb-1" />
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-1 bg-ink-0/60 rounded w-full" />
|
|
||||||
<div className="h-1 bg-ink-0/60 rounded w-5/6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-[7px] text-ink-0/80 font-bold pt-0.5">
|
|
||||||
<span>SLIDE</span>
|
|
||||||
<span className="font-extrabold">PPTX</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isWord ? (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4">
|
|
||||||
<div className="w-14 h-18 bg-ink-0 border border-ink-200 shadow-sm rounded flex flex-col justify-between p-1.5 relative overflow-hidden">
|
|
||||||
<div className="absolute top-0 right-0 left-0 bg-blue-600 text-ink-0 py-0.5 text-[8px] font-extrabold uppercase text-center tracking-wider">
|
|
||||||
DOC
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 mt-4">
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-full" />
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-5/6" />
|
|
||||||
<div className="h-1 bg-ink-200 rounded w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-[7px] text-ink-400 font-bold border-t border-ink-100 pt-0.5 mt-1">
|
|
||||||
<span>WORD</span>
|
|
||||||
<FileText className="w-2.5 h-2.5 text-blue-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : isSpreadsheet ? (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4">
|
|
||||||
<div className="w-16 h-13 bg-emerald-600 border border-emerald-700 shadow-sm rounded-lg flex flex-col justify-between p-1.5 relative overflow-hidden">
|
|
||||||
<div className="grid grid-cols-3 gap-0.5 mt-0.5">
|
|
||||||
<div className="h-1.5 bg-ink-0/90 rounded-sm" />
|
|
||||||
<div className="h-1.5 bg-ink-0/70 rounded-sm" />
|
|
||||||
<div className="h-1.5 bg-ink-0/70 rounded-sm" />
|
|
||||||
<div className="h-1.5 bg-ink-0/60 rounded-sm" />
|
|
||||||
<div className="h-1.5 bg-ink-0/80 rounded-sm" />
|
|
||||||
<div className="h-1.5 bg-ink-0/60 rounded-sm" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-[7px] text-emerald-100 font-bold pt-0.5">
|
|
||||||
<span>SHEET</span>
|
|
||||||
<span className="font-extrabold">XLSX</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full p-4">
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-ink-100 border border-ink-200 flex items-center justify-center shadow-inner relative group-hover:scale-105 transition-transform">
|
|
||||||
<Icon className="w-6 h-6 text-ink-650" />
|
|
||||||
<div className="absolute -bottom-1 -right-1 bg-ink-800 border border-ink-700 rounded px-1 py-0.5 text-[7px] font-bold text-ink-0 uppercase">
|
|
||||||
{asset.type === 'url' ? 'LINK' : 'FILE'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<div className="flex items-center gap-1.5 mb-2">
|
<div className="flex items-center gap-1.5 mb-2">
|
||||||
<div className="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-500 uppercase tracking-wider bg-ink-50 border border-ink-200">
|
<div className="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold text-ink-500 uppercase tracking-wider bg-ink-50 border border-ink-200">
|
||||||
@ -306,7 +164,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="font-bold text-ink-900 text-sm asset-card-title leading-snug line-clamp-2 group-hover:text-ink-950 transition-colors" title={asset.title}>
|
<h3 className="font-bold text-ink-900 text-sm leading-snug line-clamp-2 group-hover:text-ink-950 transition-colors" title={asset.title}>
|
||||||
{asset.title}
|
{asset.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[11px] font-medium text-ink-400 mt-1">
|
<p className="text-[11px] font-medium text-ink-400 mt-1">
|
||||||
@ -369,4 +227,3 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -101,8 +101,8 @@ export const AssetExplorer: React.FC = () => {
|
|||||||
<div className="space-y-6 text-ink-900">
|
<div className="space-y-6 text-ink-900">
|
||||||
<div className="rounded-2xl bg-ink-0 p-6 md:p-8 border border-ink-200 flex flex-col md:flex-row items-center justify-between gap-6">
|
<div className="rounded-2xl bg-ink-0 p-6 md:p-8 border border-ink-200 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-2xl explorer-title font-bold text-ink-900 md:text-3xl">Tech4Biz Asset Explorer</h1>
|
<h1 className="text-2xl font-bold text-ink-900 md:text-3xl">Tech4Biz Asset Explorer</h1>
|
||||||
<p className="text-sm explorer-desc text-ink-700 max-w-xl">
|
<p className="text-sm text-ink-700 max-w-xl">
|
||||||
Browse, preview, and download proprietary hardware IP, framework assemblies, and cloud-native building blocks licensed under your master agreements.
|
Browse, preview, and download proprietary hardware IP, framework assemblies, and cloud-native building blocks licensed under your master agreements.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -152,8 +152,8 @@ export const AssetManagement: React.FC = () => {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-ink-100 bg-ink-50 text-xs font-bold uppercase tracking-wider text-ink-600">
|
<tr className="border-b border-ink-100 bg-ink-50 text-xs font-bold uppercase tracking-wider text-ink-600">
|
||||||
<th className="px-5 py-3.5">Asset Title / Category</th>
|
<th className="px-5 py-3.5">Asset Title / Category</th>
|
||||||
<th className="px-5 py-3.5 hidden md:table-cell">Tags</th>
|
<th className="px-5 py-3.5">Tags</th>
|
||||||
<th className="px-5 py-3.5 hidden sm:table-cell">Downloads</th>
|
<th className="px-5 py-3.5">Downloads</th>
|
||||||
<th className="px-5 py-3.5">Status</th>
|
<th className="px-5 py-3.5">Status</th>
|
||||||
<th className="px-5 py-3.5 text-right">Actions</th>
|
<th className="px-5 py-3.5 text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -184,7 +184,7 @@ export const AssetManagement: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 hidden md:table-cell">
|
<td className="px-5 py-4">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{asset.tags.map(t => (
|
{asset.tags.map(t => (
|
||||||
<span key={t} className="rounded bg-ink-50 px-2 py-0.5 text-[10px] font-semibold text-ink-600 border border-ink-100">
|
<span key={t} className="rounded bg-ink-50 px-2 py-0.5 text-[10px] font-semibold text-ink-600 border border-ink-100">
|
||||||
@ -193,7 +193,7 @@ export const AssetManagement: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 font-mono font-semibold text-ink-700 hidden sm:table-cell">
|
<td className="px-5 py-4 font-mono font-semibold text-ink-700">
|
||||||
{asset.downloadsCount}
|
{asset.downloadsCount}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
|
|||||||
@ -26,30 +26,14 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
|||||||
const [isLoadingText, setIsLoadingText] = useState(false);
|
const [isLoadingText, setIsLoadingText] = useState(false);
|
||||||
const [textPreviewContent, setTextPreviewContent] = useState('');
|
const [textPreviewContent, setTextPreviewContent] = useState('');
|
||||||
|
|
||||||
const getFullAssetUrl = (url: string) => {
|
const isLocalUrl = (url: string) => {
|
||||||
let resolvedUrl = url;
|
return url.includes('localhost') || url.includes('127.0.0.1');
|
||||||
if (!url.startsWith('http')) {
|
|
||||||
const backendBase = axiosInstance.defaults.baseURL || '/api/v1';
|
|
||||||
const relativeHost = backendBase.replace('/api/v1', '');
|
|
||||||
resolvedUrl = relativeHost.startsWith('/') || relativeHost === ''
|
|
||||||
? `${window.location.origin}${relativeHost}${url}`
|
|
||||||
: `${relativeHost}${url}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If current origin is a public domain (like ngrok) but the resolved URL is local,
|
|
||||||
// rewrite the local URL host to match the public origin so that external viewers can fetch it.
|
|
||||||
const currentOrigin = window.location.origin;
|
|
||||||
const isCurrentOriginLocal = currentOrigin.includes('localhost') || currentOrigin.includes('127.0.0.1');
|
|
||||||
if (!isCurrentOriginLocal && (resolvedUrl.includes('localhost') || resolvedUrl.includes('127.0.0.1'))) {
|
|
||||||
resolvedUrl = resolvedUrl.replace(/https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/, currentOrigin);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLocalUrl = (url: string) => {
|
const getFullAssetUrl = (url: string) => {
|
||||||
const resolved = getFullAssetUrl(url);
|
return url.startsWith('http')
|
||||||
return resolved.includes('localhost') || resolved.includes('127.0.0.1');
|
? url
|
||||||
|
: `${axiosInstance.defaults.baseURL?.replace('/api/v1', '')}${url}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGithubRawUrl = (url: string) => {
|
const getGithubRawUrl = (url: string) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { updateAsset, bulkShareAssets } from '../../../services/assets-api';
|
import { updateAsset } from '../../../services/assets-api';
|
||||||
import type { Asset, Organization, ShareItem } from '../../../types/assets';
|
import type { Asset, Organization, ShareItem } from '../../../types/assets';
|
||||||
import Modal from '../../../components/ui/Modal';
|
import Modal from '../../../components/ui/Modal';
|
||||||
import Button from '../../../components/ui/Button';
|
import Button from '../../../components/ui/Button';
|
||||||
@ -11,7 +11,6 @@ interface ShareAssetModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
asset: Asset | null;
|
asset: Asset | null;
|
||||||
assetIds?: string[] | null;
|
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
}
|
}
|
||||||
@ -20,7 +19,6 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
asset,
|
asset,
|
||||||
assetIds,
|
|
||||||
organizations,
|
organizations,
|
||||||
onSuccess
|
onSuccess
|
||||||
}) => {
|
}) => {
|
||||||
@ -37,10 +35,8 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|||||||
userId: s.userId
|
userId: s.userId
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
setSharesList([]);
|
|
||||||
}
|
}
|
||||||
}, [asset, isOpen]);
|
}, [asset]);
|
||||||
|
|
||||||
const isOrgSharedEntirely = (orgId: string) => {
|
const isOrgSharedEntirely = (orgId: string) => {
|
||||||
return sharesList.some(s => s.organizationId === orgId && s.userId === null);
|
return sharesList.some(s => s.organizationId === orgId && s.userId === null);
|
||||||
@ -76,17 +72,13 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|||||||
|
|
||||||
const handleShareSubmit = async (e: React.FormEvent) => {
|
const handleShareSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!asset && (!assetIds || assetIds.length === 0)) return;
|
if (!asset) return;
|
||||||
|
|
||||||
setIsSavingShare(true);
|
setIsSavingShare(true);
|
||||||
try {
|
try {
|
||||||
if (asset) {
|
await updateAsset(asset.id, {
|
||||||
await updateAsset(asset.id, {
|
shares: sharesList
|
||||||
shares: sharesList
|
});
|
||||||
});
|
|
||||||
} else if (assetIds && assetIds.length > 0) {
|
|
||||||
await bulkShareAssets(assetIds, sharesList);
|
|
||||||
}
|
|
||||||
success('Share permissions updated', 'The asset visibility settings have been updated.');
|
success('Share permissions updated', 'The asset visibility settings have been updated.');
|
||||||
onSuccess();
|
onSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
@ -100,10 +92,10 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen && (!!asset || (!!assetIds && assetIds.length > 0))}
|
isOpen={isOpen && !!asset}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title="Share Settings"
|
title="Share Settings"
|
||||||
subtitle={asset ? asset.title : `${assetIds?.length || 0} selected assets`}
|
subtitle={asset?.title}
|
||||||
size="md"
|
size="md"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
@ -127,7 +119,7 @@ export const ShareAssetModal: React.FC<ShareAssetModalProps> = ({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(asset || (assetIds && assetIds.length > 0)) && (
|
{asset && (
|
||||||
<form id="share-asset-form" onSubmit={handleShareSubmit} className="space-y-4">
|
<form id="share-asset-form" onSubmit={handleShareSubmit} className="space-y-4">
|
||||||
<p className="text-xs text-ink-600 leading-relaxed font-sans">
|
<p className="text-xs text-ink-600 leading-relaxed font-sans">
|
||||||
Select organizations or expand to specify exact users that can access this asset:
|
Select organizations or expand to specify exact users that can access this asset:
|
||||||
|
|||||||
@ -95,12 +95,14 @@ const OrbitingRing: React.FC = () => (
|
|||||||
{/* Center icon */}
|
{/* Center icon */}
|
||||||
<div className="absolute inset-0 flex items-center justify-center z-10">
|
<div className="absolute inset-0 flex items-center justify-center z-10">
|
||||||
<div
|
<div
|
||||||
className="w-16 h-16 rounded-2xl flex items-center justify-center animate-pulse-glow bg-white border border-ink-200 shadow-sm p-2"
|
className="w-16 h-16 rounded-2xl flex items-center justify-center animate-pulse-glow"
|
||||||
style={{
|
style={{
|
||||||
|
background: 'linear-gradient(135deg, var(--color-primary-400), var(--color-primary-600))',
|
||||||
|
boxShadow: '0 0 30px rgba(162,231,113,0.5)',
|
||||||
transform: 'perspective(300px) rotateY(-8deg) rotateX(4deg)',
|
transform: 'perspective(300px) rotateY(-8deg) rotateX(4deg)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
|
<ShieldCheck className="w-8 h-8" style={{ color: 'var(--color-ink-800)' }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -258,10 +260,10 @@ export const LoginForm: React.FC = () => {
|
|||||||
<OrbitingRing />
|
<OrbitingRing />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl login-form-title font-extrabold tracking-tight" style={{ color: 'var(--color-ink-800)' }}>
|
<h1 className="text-2xl font-extrabold tracking-tight" style={{ color: 'var(--color-ink-800)' }}>
|
||||||
{mfaPendingEmail ? 'Verify Identity' : 'Tech4Biz Portal'}
|
{mfaPendingEmail ? 'Verify Identity' : 'Tech4Biz Portal'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm login-form-subtitle mt-1.5" style={{ color: 'var(--color-ink-600)' }}>
|
<p className="text-sm mt-1.5" style={{ color: 'var(--color-ink-600)' }}>
|
||||||
{mfaPendingEmail
|
{mfaPendingEmail
|
||||||
? `Code sent to ${mfaPendingEmail}`
|
? `Code sent to ${mfaPendingEmail}`
|
||||||
: 'Enterprise hardware & software asset distribution'}
|
: 'Enterprise hardware & software asset distribution'}
|
||||||
|
|||||||
1
Channel-Frontend/src/fontsource.d.ts
vendored
1
Channel-Frontend/src/fontsource.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
declare module '@fontsource-variable/inter';
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
--color-info: #3b82f6;
|
--color-info: #3b82f6;
|
||||||
|
|
||||||
/* ── Typography ── */
|
/* ── Typography ── */
|
||||||
--font-sans: 'Inter Variable', 'Inter', ui-sans-serif, system-ui, sans-serif;
|
--font-sans: 'Outfit', 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||||
|
|
||||||
/* ── Spacing Scale ── */
|
/* ── Spacing Scale ── */
|
||||||
@ -594,71 +594,3 @@ body {
|
|||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
animation: shimmer 1.5s linear infinite;
|
animation: shimmer 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
|
||||||
TYPOGRAPHY FINE-TUNING (+30% size increase)
|
|
||||||
============================================================ */
|
|
||||||
.page-title {
|
|
||||||
/* text-xl fallback: 1.25rem (20px) * 1.3 = 1.625rem (26px) */
|
|
||||||
font-size: 1.625rem !important;
|
|
||||||
}
|
|
||||||
.page-subtitle {
|
|
||||||
/* text-xs fallback: 0.75rem (12px) * 1.3 = 0.975rem (15.6px) */
|
|
||||||
font-size: 0.975rem !important;
|
|
||||||
}
|
|
||||||
.action-card-title {
|
|
||||||
/* text-lg fallback: 1.125rem (18px) * 1.3 = 1.4625rem (23.4px) */
|
|
||||||
font-size: 1.4625rem !important;
|
|
||||||
}
|
|
||||||
.action-card-desc {
|
|
||||||
/* text-xs fallback: 0.75rem (12px) * 1.3 = 0.975rem (15.6px) */
|
|
||||||
font-size: 0.975rem !important;
|
|
||||||
}
|
|
||||||
.asset-card-title {
|
|
||||||
/* text-sm fallback: 0.875rem (14px) * 1.3 = 1.1375rem (18.2px) */
|
|
||||||
font-size: 1.1375rem !important;
|
|
||||||
}
|
|
||||||
.explorer-title {
|
|
||||||
/* text-2xl fallback: 1.5rem (24px) * 1.3 = 1.95rem (31.2px) */
|
|
||||||
font-size: 1.95rem !important;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.explorer-title {
|
|
||||||
/* md:text-3xl fallback: 1.875rem (30px) * 1.3 = 2.4375rem (39px) */
|
|
||||||
font-size: 2.4375rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.explorer-desc {
|
|
||||||
/* text-sm fallback: 0.875rem (14px) * 1.3 = 1.1375rem (18.2px) */
|
|
||||||
font-size: 1.1375rem !important;
|
|
||||||
}
|
|
||||||
.agreement-card-title {
|
|
||||||
/* text-base fallback: 1.0rem (16px) * 1.3 = 1.3rem (20.8px) */
|
|
||||||
font-size: 1.3rem !important;
|
|
||||||
}
|
|
||||||
.agreement-card-desc {
|
|
||||||
/* text-xs fallback: 0.75rem (12px) * 1.3 = 0.975rem (15.6px) */
|
|
||||||
font-size: 0.975rem !important;
|
|
||||||
}
|
|
||||||
.login-title {
|
|
||||||
/* text-3xl fallback: 1.875rem (30px) * 1.3 = 2.4375rem (39px) */
|
|
||||||
font-size: 2.4375rem !important;
|
|
||||||
}
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.login-title {
|
|
||||||
/* lg:text-4xl fallback: 2.25rem (36px) * 1.3 = 2.925rem (46.8px) */
|
|
||||||
font-size: 2.925rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.login-desc {
|
|
||||||
/* text-sm fallback: 0.875rem (14px) * 1.3 = 1.1375rem (18.2px) */
|
|
||||||
font-size: 1.1375rem !important;
|
|
||||||
}
|
|
||||||
.login-form-title {
|
|
||||||
/* text-2xl fallback: 1.5rem (24px) * 1.3 = 1.95rem (31.2px) */
|
|
||||||
font-size: 1.95rem !important;
|
|
||||||
}
|
|
||||||
.login-form-subtitle {
|
|
||||||
/* text-sm fallback: 0.875rem (14px) * 1.3 = 1.1375rem (18.2px) */
|
|
||||||
font-size: 1.1375rem !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import '@fontsource-variable/inter'
|
|
||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import type { Variants } from "framer-motion";
|
import type { Variants } from "framer-motion";
|
||||||
import { UploadCloud, Search, File, CheckCircle, Share2 } from "lucide-react";
|
import { UploadCloud, Search, File, CheckCircle } from "lucide-react";
|
||||||
import { useAuthStore } from "../hooks/use-auth";
|
import { useAuthStore } from "../hooks/use-auth";
|
||||||
import { axiosInstance } from "../services/axios";
|
import { axiosInstance } from "../services/axios";
|
||||||
import {
|
import {
|
||||||
@ -66,7 +66,6 @@ export const AssetsPage = () => {
|
|||||||
|
|
||||||
const [activeAsset, setActiveAsset] = useState<Asset | null>(null);
|
const [activeAsset, setActiveAsset] = useState<Asset | null>(null);
|
||||||
const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
|
const [activeMenuId, setActiveMenuId] = useState<string | null>(null);
|
||||||
const [selectedAssetIds, setSelectedAssetIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -77,7 +76,6 @@ export const AssetsPage = () => {
|
|||||||
try {
|
try {
|
||||||
const assetsData = await getAssets();
|
const assetsData = await getAssets();
|
||||||
setAssets(assetsData);
|
setAssets(assetsData);
|
||||||
setSelectedAssetIds([]);
|
|
||||||
|
|
||||||
if (user?.role === "ADMIN") {
|
if (user?.role === "ADMIN") {
|
||||||
const orgsData = await getOrganizations();
|
const orgsData = await getOrganizations();
|
||||||
@ -96,19 +94,10 @@ export const AssetsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openShareModal = (asset: Asset) => {
|
const openShareModal = (asset: Asset) => {
|
||||||
setSelectedAssetIds([]);
|
|
||||||
setActiveAsset(asset);
|
setActiveAsset(asset);
|
||||||
setIsShareOpen(true);
|
setIsShareOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleSelectAsset = (assetId: string) => {
|
|
||||||
setSelectedAssetIds((prev) =>
|
|
||||||
prev.includes(assetId)
|
|
||||||
? prev.filter((id) => id !== assetId)
|
|
||||||
: [...prev, assetId]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openViewerModal = (asset: Asset) => {
|
const openViewerModal = (asset: Asset) => {
|
||||||
setActiveAsset(asset);
|
setActiveAsset(asset);
|
||||||
setIsViewerOpen(true);
|
setIsViewerOpen(true);
|
||||||
@ -299,20 +288,6 @@ export const AssetsPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{user?.role === "ADMIN" && selectedAssetIds.length > 0 && (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveAsset(null);
|
|
||||||
setIsShareOpen(true);
|
|
||||||
}}
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
icon={<Share2 className="w-3.5 h-3.5" />}
|
|
||||||
>
|
|
||||||
Share Selected ({selectedAssetIds.length})
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{user?.role === "ADMIN" && (
|
{user?.role === "ADMIN" && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setIsUploadOpen(true)}
|
onClick={() => setIsUploadOpen(true)}
|
||||||
@ -363,8 +338,6 @@ export const AssetsPage = () => {
|
|||||||
onOpenViewer={openViewerModal}
|
onOpenViewer={openViewerModal}
|
||||||
onDownload={handleDownload}
|
onDownload={handleDownload}
|
||||||
onRequestDownload={handleRequestDownload}
|
onRequestDownload={handleRequestDownload}
|
||||||
isSelected={selectedAssetIds.includes(asset.id)}
|
|
||||||
onToggleSelect={handleToggleSelectAsset}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
@ -394,10 +367,8 @@ export const AssetsPage = () => {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsShareOpen(false);
|
setIsShareOpen(false);
|
||||||
setActiveAsset(null);
|
setActiveAsset(null);
|
||||||
setSelectedAssetIds([]);
|
|
||||||
}}
|
}}
|
||||||
asset={activeAsset}
|
asset={activeAsset}
|
||||||
assetIds={selectedAssetIds}
|
|
||||||
organizations={organizations}
|
organizations={organizations}
|
||||||
onSuccess={fetchData}
|
onSuccess={fetchData}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
const ndaAcceptance = acceptances?.find(a => a.document.type === 'NDA');
|
const ndaAcceptance = acceptances?.find(a => a.document.type === 'NDA');
|
||||||
const msaAcceptance = acceptances?.find(a => a.document.type === 'MSA');
|
const msaAcceptance = acceptances?.find(a => a.document.type === 'MSA');
|
||||||
|
|
||||||
const fileHost = (import.meta.env.VITE_API_URL || '/api/v1').replace('/api/v1', '');
|
const fileHost = (import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1').replace('/api/v1', '');
|
||||||
|
|
||||||
// Header component
|
// Header component
|
||||||
const headerNode = (
|
const headerNode = (
|
||||||
@ -80,8 +80,8 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-base agreement-card-title font-extrabold text-ink-900">Mutual Non-Disclosure Agreement (NDA)</h3>
|
<h3 className="text-base font-extrabold text-ink-900">Mutual Non-Disclosure Agreement (NDA)</h3>
|
||||||
<p className="text-xs agreement-card-desc text-ink-500 mt-1.5 leading-relaxed font-semibold">
|
<p className="text-xs text-ink-500 mt-1.5 leading-relaxed font-semibold">
|
||||||
Required to protect proprietary IP, silicon designs, and private data sharing during development.
|
Required to protect proprietary IP, silicon designs, and private data sharing during development.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -155,8 +155,8 @@ export const ClientAgreementsPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-base agreement-card-title font-extrabold text-ink-900">Master Services Agreement (MSA)</h3>
|
<h3 className="text-base font-extrabold text-ink-900">Master Services Agreement (MSA)</h3>
|
||||||
<p className="text-xs agreement-card-desc text-ink-500 mt-1.5 leading-relaxed font-semibold">
|
<p className="text-xs text-ink-500 mt-1.5 leading-relaxed font-semibold">
|
||||||
Defines the commercial framework, SLA guidelines, and consulting provisions for the partnership.
|
Defines the commercial framework, SLA guidelines, and consulting provisions for the partnership.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@ -81,29 +81,27 @@ export const DashboardPage = () => {
|
|||||||
className="p-5 space-y-6 flex flex-col min-h-0 flex-1 overflow-y-auto"
|
className="p-5 space-y-6 flex flex-col min-h-0 flex-1 overflow-y-auto"
|
||||||
>
|
>
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
{user?.role === 'ADMIN' && (
|
<motion.div variants={itemVariants} className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
|
||||||
<motion.div variants={itemVariants} className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
|
{[
|
||||||
{[
|
{ label: 'Global Network Uptime', value: '99.99%', icon: Activity, trend: '+0.01%' },
|
||||||
{ label: 'Global Network Uptime', value: '99.99%', icon: Activity, trend: '+0.01%' },
|
{ label: 'Active Data Streams', value: '1,492', icon: Zap, trend: '+12%' },
|
||||||
{ label: 'Active Data Streams', value: '1,492', icon: Zap, trend: '+12%' },
|
{ label: 'Security Compliance', value: 'Level 4', icon: ShieldCheck, trend: 'Verified' }
|
||||||
{ label: 'Security Compliance', value: 'Level 4', icon: ShieldCheck, trend: 'Verified' }
|
].map((stat, i) => (
|
||||||
].map((stat, i) => (
|
<div key={i} className="relative group overflow-hidden rounded-xl bg-ink-0 border border-ink-200 p-4 transition-all duration-300 shadow-sm hover:shadow-md">
|
||||||
<div key={i} className="relative group overflow-hidden rounded-xl bg-ink-0 border border-ink-200 p-4 transition-all duration-300 shadow-sm hover:shadow-md">
|
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity duration-500 group-hover:scale-110 transform">
|
||||||
<div className="absolute top-0 right-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity duration-500 group-hover:scale-110 transform">
|
<stat.icon className="w-16 h-16 text-ink-900" />
|
||||||
<stat.icon className="w-16 h-16 text-ink-900" />
|
|
||||||
</div>
|
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-ink-500 mb-1">{stat.label}</p>
|
|
||||||
<div className="flex items-end gap-3 mt-2">
|
|
||||||
<h3 className="text-xl font-bold text-ink-900 tracking-tight">{stat.value}</h3>
|
|
||||||
<span className="text-xs font-bold text-ink-900 bg-ink-100 px-2 py-0.5 rounded-md mb-0.5 border border-ink-200">{stat.trend}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<p className="text-xs font-semibold uppercase tracking-wider text-ink-500 mb-1">{stat.label}</p>
|
||||||
</motion.div>
|
<div className="flex items-end gap-3 mt-2">
|
||||||
)}
|
<h3 className="text-xl font-bold text-ink-900 tracking-tight">{stat.value}</h3>
|
||||||
|
<span className="text-xs font-bold text-ink-900 bg-ink-100 px-2 py-0.5 rounded-md mb-0.5 border border-ink-200">{stat.trend}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Main Action Cards */}
|
{/* Main Action Cards */}
|
||||||
<motion.div variants={itemVariants} className={`grid grid-cols-1 ${CARDS.length === 2 ? 'md:grid-cols-2' : 'md:grid-cols-3'} gap-4 pt-2`}>
|
<motion.div variants={itemVariants} className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
|
||||||
{CARDS.map((card, idx) => (
|
{CARDS.map((card, idx) => (
|
||||||
<Link key={idx} to={card.path} className="group relative block h-full">
|
<Link key={idx} to={card.path} className="group relative block h-full">
|
||||||
<div className="relative h-full bg-ink-0 border border-ink-200 rounded-xl p-5 hover:border-ink-400 transition-all duration-300 overflow-hidden shadow-sm hover:shadow-md hover:-translate-y-0.5 flex flex-col justify-between">
|
<div className="relative h-full bg-ink-0 border border-ink-200 rounded-xl p-5 hover:border-ink-400 transition-all duration-300 overflow-hidden shadow-sm hover:shadow-md hover:-translate-y-0.5 flex flex-col justify-between">
|
||||||
@ -121,10 +119,10 @@ export const DashboardPage = () => {
|
|||||||
<div className="inline-block px-2 py-0.5 rounded-md bg-ink-50 border border-ink-200 text-xs font-bold text-ink-500 mb-3 shadow-sm">
|
<div className="inline-block px-2 py-0.5 rounded-md bg-ink-50 border border-ink-200 text-xs font-bold text-ink-500 mb-3 shadow-sm">
|
||||||
{card.metrics}
|
{card.metrics}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg action-card-title font-bold text-ink-900 mb-2 tracking-tight">
|
<h3 className="text-lg font-bold text-ink-900 mb-2 tracking-tight">
|
||||||
{card.title}
|
{card.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-ink-500 action-card-desc leading-normal text-xs font-medium">
|
<p className="text-ink-500 leading-normal text-xs font-medium">
|
||||||
{card.description}
|
{card.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Shield, CheckCircle, AlertCircle, ChevronRight, KeyRound, Eye, EyeOff } from 'lucide-react';
|
import { Shield, CheckCircle, AlertCircle, ChevronRight, KeyRound, Eye, EyeOff } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
import { axiosInstance } from '../services/axios';
|
import { axiosInstance } from '../services/axios';
|
||||||
import { useAuthStore } from '../hooks/use-auth';
|
import { useAuthStore } from '../hooks/use-auth';
|
||||||
import { useToast } from '../hooks/use-toast';
|
import { useToast } from '../hooks/use-toast';
|
||||||
|
|
||||||
export const InvitePage: React.FC = () => {
|
export const InvitePage: React.FC = () => {
|
||||||
const { success } = useToast();
|
const { success, error: toastError } = useToast();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -28,10 +29,9 @@ export const InvitePage: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify token on mount
|
const validateToken = async () => {
|
||||||
const verifyToken = async () => {
|
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(`/auth/verify-invite?token=${token}`);
|
const response = await axios.get(`${import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1'}/auth/invite/${token}`);
|
||||||
setEmail(response.data.email);
|
setEmail(response.data.email);
|
||||||
setStatus('valid');
|
setStatus('valid');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -39,7 +39,7 @@ export const InvitePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
verifyToken();
|
validateToken();
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
@ -51,36 +51,25 @@ export const InvitePage: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 8) {
|
if (password.length < 6) {
|
||||||
setError('Password must be at least 8 characters long');
|
setError('Password must be at least 6 characters');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.post('/auth/activate-partner', {
|
const response = await axiosInstance.post('/auth/invite/accept', {
|
||||||
token,
|
token,
|
||||||
password
|
password
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success toast
|
setAuth(response.data);
|
||||||
success('Account activated successfully!');
|
success("Account created successfully", "Please complete your partner onboarding profile.");
|
||||||
|
navigate('/onboarding');
|
||||||
// Set auth store
|
|
||||||
setAuth({ user: response.data.user, accessToken: response.data.token });
|
|
||||||
|
|
||||||
// Redirect based on role / status
|
|
||||||
setTimeout(() => {
|
|
||||||
if (response.data.user.role === 'ADMIN') {
|
|
||||||
navigate('/admin');
|
|
||||||
} else if (response.data.user.onboardingStatus === 'APPROVED') {
|
|
||||||
navigate('/client');
|
|
||||||
} else {
|
|
||||||
navigate('/onboarding');
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.detail || 'Activation failed');
|
const errMsg = err.response?.data?.error || 'Failed to accept invite';
|
||||||
|
setError(errMsg);
|
||||||
|
toastError("Failed to accept invite", errMsg);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -88,7 +77,7 @@ export const InvitePage: React.FC = () => {
|
|||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-ink-50 flex items-center justify-center">
|
<div className="min-h-screen bg-ink-50 flex items-center justify-center">
|
||||||
<div className="w-8 h-8 border-4 border-ink-900/35 border-t-ink-900 rounded-full animate-spin" />
|
<div className="w-8 h-8 border-4 border-ink-900/30 border-t-ink-900 rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -96,13 +85,13 @@ export const InvitePage: React.FC = () => {
|
|||||||
if (status === 'invalid') {
|
if (status === 'invalid') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-ink-50 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-ink-50 flex items-center justify-center p-4">
|
||||||
<div className="w-full max-w-md bg-ink-0 rounded-[2rem] p-8 border border-ink-200 shadow-2xl text-center">
|
<div className="w-full max-w-md bg-ink-0 rounded-3xl p-8 border border-ink-200 text-center shadow-2xl">
|
||||||
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
<AlertCircle className="w-8 h-8 text-red-600 dark:text-red-500" />
|
<AlertCircle className="w-8 h-8 text-red-600 dark:text-red-500" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl login-form-title font-bold text-ink-900 mb-2">Invalid or Expired Link</h2>
|
<h2 className="text-2xl font-bold text-ink-900 mb-2">Invalid or Expired Link</h2>
|
||||||
<p className="text-sm login-form-subtitle text-ink-500 max-w-md mx-auto mb-6">
|
<p className="text-ink-500 text-sm mb-8">
|
||||||
The onboarding invitation link is invalid, expired, or has already been used. Please request a new invitation from your administrator.
|
This invitation link is no longer valid. Please request a new invitation from your administrator.
|
||||||
</p>
|
</p>
|
||||||
<button onClick={() => navigate('/login')} className="text-ink-900 font-extrabold hover:underline">
|
<button onClick={() => navigate('/login')} className="text-ink-900 font-extrabold hover:underline">
|
||||||
Return to Login
|
Return to Login
|
||||||
@ -118,12 +107,12 @@ export const InvitePage: React.FC = () => {
|
|||||||
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-ink-900/5 rounded-full blur-[120px] pointer-events-none" />
|
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-ink-900/5 rounded-full blur-[120px] pointer-events-none" />
|
||||||
|
|
||||||
<div className="w-full max-w-md z-10">
|
<div className="w-full max-w-md z-10">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-10">
|
||||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-ink-900 to-ink-800 shadow-xl flex items-center justify-center mx-auto mb-6">
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-ink-900 to-ink-800 shadow-xl flex items-center justify-center mx-auto mb-6">
|
||||||
<Shield className="w-6 h-6 text-ink-0" />
|
<Shield className="w-6 h-6 text-ink-0" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl login-title font-extrabold tracking-tight mb-2">Welcome to Tech4Biz</h1>
|
<h1 className="text-3xl font-extrabold tracking-tight mb-2">Welcome to Tech4Biz</h1>
|
||||||
<p className="text-sm login-desc text-ink-500">
|
<p className="text-sm font-medium text-ink-500">
|
||||||
Set up your partner account for <span className="text-ink-900 font-bold">{email}</span>
|
Set up your partner account for <span className="text-ink-900 font-bold">{email}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { loginUser } from "../services/auth-api";
|
|||||||
import { useAuthStore } from "../hooks/use-auth";
|
import { useAuthStore } from "../hooks/use-auth";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Lock, Mail, ArrowRight, Eye, EyeOff } from "lucide-react";
|
import { Hexagon, Lock, Mail, ArrowRight, Eye, EyeOff } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useToast } from "../hooks/use-toast";
|
import { useToast } from "../hooks/use-toast";
|
||||||
|
|
||||||
@ -71,14 +71,15 @@ export const LoginPage = () => {
|
|||||||
>
|
>
|
||||||
{/* Left Column - Partner Explanation */}
|
{/* Left Column - Partner Explanation */}
|
||||||
<div className="md:col-span-6 lg:col-span-7 flex flex-col justify-center text-left">
|
<div className="md:col-span-6 lg:col-span-7 flex flex-col justify-center text-left">
|
||||||
<div className="space-y-4 max-w-xl">
|
<h1 className="text-3xl lg:text-4xl font-extrabold text-ink-900 tracking-tight leading-tight">
|
||||||
<h1 className="text-3xl login-title lg:text-4xl font-extrabold text-ink-900 tracking-tight leading-tight">
|
<span className="gradient-text">Tech4Biz Channel Partner</span>
|
||||||
Enterprise Scale, Secure Delivery
|
</h1>
|
||||||
</h1>
|
<p className="text-ink-500 text-sm mt-5 font-medium leading-relaxed max-w-lg">
|
||||||
<p className="text-ink-500 text-sm login-desc mt-5 font-medium leading-relaxed max-w-lg">
|
Expand your business by partnering with Tech4Biz and unlock new
|
||||||
Tech4Biz Channel Partner Portal facilitates authenticated, low-latency distribution of physical asset coordinates, design assets, and cryptographically verified legal agreements.
|
opportunities for growth through our innovative technology
|
||||||
</p>
|
solutions. Join our partner network to access exclusive resources,
|
||||||
</div>
|
dedicated support, and a platform designed to help you succeed.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Login Component */}
|
{/* Right Column - Login Component */}
|
||||||
@ -87,11 +88,11 @@ export const LoginPage = () => {
|
|||||||
<div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-ink-300 to-transparent" />
|
<div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-ink-300 to-transparent" />
|
||||||
|
|
||||||
<div className="flex flex-col items-center mb-8 text-center">
|
<div className="flex flex-col items-center mb-8 text-center">
|
||||||
<div className="relative flex items-center justify-center w-16 h-16 rounded-2xl bg-ink-0 border border-ink-200 shadow-sm mb-6 p-2">
|
<div className="relative flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-tr from-ink-900 to-ink-800 shadow-md mb-6">
|
||||||
<img src="/logo.png" alt="Tech4Biz" className="w-full h-full object-contain" />
|
<Hexagon className="text-ink-0 w-8 h-8 absolute" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl login-form-title font-bold text-ink-900 tracking-tight">
|
<h2 className="text-2xl font-bold text-ink-900 tracking-tight">
|
||||||
Partner Identity Gateway
|
Channel Portal
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-ink-500 text-[10px] mt-1.5 font-bold uppercase tracking-wider">
|
<p className="text-ink-500 text-[10px] mt-1.5 font-bold uppercase tracking-wider">
|
||||||
Authorized Access Only
|
Authorized Access Only
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export const OnboardingPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDocumentViewer = (type: DocumentType) => {
|
const renderDocumentViewer = (type: DocumentType) => {
|
||||||
const fileHost = (import.meta.env.VITE_API_URL || '/api/v1').replace('/api/v1', '');
|
const fileHost = (import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1').replace('/api/v1', '');
|
||||||
const pdfUrl = type === 'NDA' ? ndaPdfUrl : msaPdfUrl;
|
const pdfUrl = type === 'NDA' ? ndaPdfUrl : msaPdfUrl;
|
||||||
|
|
||||||
if (pdfUrl) {
|
if (pdfUrl) {
|
||||||
@ -401,9 +401,9 @@ export const OnboardingPage: React.FC = () => {
|
|||||||
<Lock className="w-3.5 h-3.5 text-ink-0" />
|
<Lock className="w-3.5 h-3.5 text-ink-0" />
|
||||||
<span className="text-[10px] font-bold text-ink-0 tracking-widest uppercase">Action Required</span>
|
<span className="text-[10px] font-bold text-ink-0 tracking-widest uppercase">Action Required</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl login-title font-extrabold tracking-tight mb-2">Non-Disclosure Agreement</h2>
|
<h2 className="text-3xl font-extrabold tracking-tight mb-2">Non-Disclosure Agreement</h2>
|
||||||
<p className="text-sm login-desc text-ink-500 mt-1.5 font-medium max-w-md">
|
<p className="text-sm font-medium text-ink-500">
|
||||||
Please review the mutual NDA terms carefully before signing.
|
Please provide your signature or upload a signed copy of our standard NDA to proceed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -431,9 +431,9 @@ export const OnboardingPage: React.FC = () => {
|
|||||||
<FileText className="w-3.5 h-3.5 text-ink-600" />
|
<FileText className="w-3.5 h-3.5 text-ink-600" />
|
||||||
<span className="text-[10px] font-bold text-ink-700 tracking-widest uppercase">Final Agreement</span>
|
<span className="text-[10px] font-bold text-ink-700 tracking-widest uppercase">Final Agreement</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl login-title font-extrabold tracking-tight mb-2">Master Services Agreement</h2>
|
<h2 className="text-3xl font-extrabold tracking-tight mb-2">Master Services Agreement</h2>
|
||||||
<p className="text-sm login-desc text-ink-500 mt-1.5 font-medium max-w-md">
|
<p className="text-sm font-medium text-ink-500">
|
||||||
Please review the master services partnership terms before signing.
|
Sign the MSA to finalize your compliance requirements and enter the approval queue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -341,8 +341,8 @@ export const DirectoryPage: React.FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th className="px-5 py-3">Partner</th>
|
<th className="px-5 py-3">Partner</th>
|
||||||
<th className="px-5 py-3">Status</th>
|
<th className="px-5 py-3">Status</th>
|
||||||
<th className="px-5 py-3 hidden sm:table-cell">MFA</th>
|
<th className="px-5 py-3">MFA</th>
|
||||||
<th className="px-5 py-3 hidden sm:table-cell">Joined</th>
|
<th className="px-5 py-3">Joined</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-ink-200 bg-ink-0">
|
<tbody className="divide-y divide-ink-200 bg-ink-0">
|
||||||
@ -391,7 +391,7 @@ export const DirectoryPage: React.FC = () => {
|
|||||||
{sc.label}
|
{sc.label}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 hidden sm:table-cell">
|
<td className="px-5 py-4">
|
||||||
{partner.mfaEnabled ? (
|
{partner.mfaEnabled ? (
|
||||||
<span className="text-xs font-bold text-ink-900">
|
<span className="text-xs font-bold text-ink-900">
|
||||||
Enabled
|
Enabled
|
||||||
@ -402,7 +402,7 @@ export const DirectoryPage: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 text-xs text-ink-500 font-medium hidden sm:table-cell">
|
<td className="px-5 py-4 text-xs text-ink-500 font-medium">
|
||||||
{new Date(partner.createdAt).toLocaleDateString()}
|
{new Date(partner.createdAt).toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export const LegalTemplatesPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const currentDoc = activeTab === 'NDA' ? ndaDoc : msaDoc;
|
const currentDoc = activeTab === 'NDA' ? ndaDoc : msaDoc;
|
||||||
const fileHost = (import.meta.env.VITE_API_URL || '/api/v1').replace('/api/v1', '');
|
const fileHost = (import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1').replace('/api/v1', '');
|
||||||
|
|
||||||
// Header component
|
// Header component
|
||||||
const headerNode = (
|
const headerNode = (
|
||||||
|
|||||||
@ -52,10 +52,3 @@ export const rejectDownloadRequest = async (assetId: string, requestId: string):
|
|||||||
export const downloadAssetFile = async (id: string): Promise<void> => {
|
export const downloadAssetFile = async (id: string): Promise<void> => {
|
||||||
await axiosInstance.post(`/assets/${id}/download`);
|
await axiosInstance.post(`/assets/${id}/download`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bulkShareAssets = async (
|
|
||||||
assetIds: string[],
|
|
||||||
shares: Array<{ organizationId: string; userId: string | null }>
|
|
||||||
): Promise<void> => {
|
|
||||||
await axiosInstance.patch('/assets/bulk-share', { assetIds, shares });
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
export const axiosInstance = axios.create({
|
export const axiosInstance = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || '/api/v1',
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,21 +7,8 @@ export default defineConfig({
|
|||||||
plugins: [tailwindcss(), react()],
|
plugins: [tailwindcss(), react()],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: [
|
allowedHosts: [
|
||||||
"toughly-coinstantaneous-dimple.ngrok-free.dev",
|
"toughly-coinstantaneous-dimple.ngrok-free.dev"
|
||||||
"spruce-fridge-destiny.ngrok-free.dev"
|
|
||||||
],
|
],
|
||||||
cors: true,
|
cors: true
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:5000',
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
|
||||||
'/uploads': {
|
|
||||||
target: 'http://localhost:5000',
|
|
||||||
changeOrigin: true,
|
|
||||||
secure: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
88
README.md
88
README.md
@ -1,76 +1,32 @@
|
|||||||
# Tech4Biz Channel Partner Onboarding & Secure Asset Management
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
An enterprise-grade, secure, multi-tenant portal designed for onboarding channel partners, managing digital assets, and orchestrating NDA/MSA legal workflows.
|
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||||
|
|
||||||
---
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
## 🛠️ Technology Stack
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
* **Frontend**: React (Vite, TypeScript, Tailwind CSS, Axios)
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
* **Backend**: Node.js (Express, TypeScript, Prisma ORM, JWT, Helmet)
|
|
||||||
* **Databases**: PostgreSQL (Relational metadata storage)
|
|
||||||
* **Object Storage**: MinIO (Private, S3-compatible asset storage container)
|
|
||||||
|
|
||||||
---
|
## React Compiler
|
||||||
|
|
||||||
## 🚀 Quick Start (Development)
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
### Prerequisites
|
## Expanding the Oxlint configuration
|
||||||
* Ensure **Docker** and **Node.js** are installed and running.
|
|
||||||
* The startup scripts automatically detect and terminate any processes occupying backend/frontend ports to prevent crash loops.
|
|
||||||
|
|
||||||
### Step 1: Start Backend, Database, & Storage
|
If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`:
|
||||||
From the project root:
|
|
||||||
```bash
|
|
||||||
./run-backend.sh
|
|
||||||
```
|
|
||||||
This script will:
|
|
||||||
1. Spin up the Postgres and MinIO containers.
|
|
||||||
2. Generate environment variables.
|
|
||||||
3. Align the database schemas via Prisma.
|
|
||||||
4. Run database seeders (Admin/Partner mock accounts).
|
|
||||||
5. **Automatically seed the MinIO bucket with static documents.**
|
|
||||||
6. Free port `5000` (if in use) and launch the backend server.
|
|
||||||
|
|
||||||
### Step 2: Start Frontend App
|
```json
|
||||||
From the project root:
|
{
|
||||||
```bash
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
./run-frontend.sh
|
"plugins": ["react", "typescript", "oxc"],
|
||||||
```
|
"options": {
|
||||||
This script will resolve dependencies, free port `5173` (if in use), and launch the Vite development server on [http://localhost:5173](http://localhost:5173).
|
"typeAware": true
|
||||||
|
},
|
||||||
---
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
## 📂 MinIO Seeding & Data Transfer
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
### How Asset Seeding Works
|
}
|
||||||
To ensure that digital assets (like product guides and NDA templates) are transferred when cloning the repository:
|
|
||||||
1. Standard assets are committed to the **`minio-seed/`** directory in the repository root (not ignored by Git).
|
|
||||||
2. During server start (via `./run-backend.sh`), the backend runs the S3 seeder command:
|
|
||||||
```bash
|
|
||||||
npx ts-node src/seed-minio.ts
|
|
||||||
```
|
|
||||||
3. The script initializes the `secure-assets` bucket in MinIO and uploads any files found in the local `/minio-seed/` folder to the bucket, preserving exact filenames, MIME types, and sizes.
|
|
||||||
|
|
||||||
### Manual Seeding / Restore Command
|
|
||||||
If you ever reset your MinIO volume and need to re-populate the S3 bucket with the committed repository assets, run the following commands:
|
|
||||||
```bash
|
|
||||||
cd Channel-Backend
|
|
||||||
npx ts-node src/seed-minio.ts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories.
|
||||||
|
|
||||||
## 🔑 Default Credentials & Admin Ports
|
|
||||||
|
|
||||||
### Web Interfaces
|
|
||||||
* **Frontend Dashboard**: [http://localhost:5173](http://localhost:5173)
|
|
||||||
* **MinIO Console Dashboard**: [http://localhost:9090](http://localhost:9090)
|
|
||||||
|
|
||||||
### Login Credentials
|
|
||||||
| System | User/ID | Password/Secret |
|
|
||||||
| :--- | :--- | :--- |
|
|
||||||
| **Admin Account** | `admin@tech4biz.com` | `Password123` |
|
|
||||||
| **Partner (Approved)** | `active-partner@partner.com` | `Password123` |
|
|
||||||
| **Partner (Pending NDA)** | `pending-onboarding@partner.com` | `Password123` |
|
|
||||||
| **MinIO Console** | `minio_admin` | `minio_password_2026` |
|
|
||||||
| **JWT Secrets** | *Auto-generated in `.env`* | `super-secret-jwt-key-2026-world-class` |
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 283 KiB |
Binary file not shown.
Binary file not shown.
@ -76,9 +76,6 @@ npx prisma db push
|
|||||||
info "Running database seeding..."
|
info "Running database seeding..."
|
||||||
npx ts-node seed.ts
|
npx ts-node seed.ts
|
||||||
|
|
||||||
info "Running MinIO storage seeding..."
|
|
||||||
npx ts-node src/seed-minio.ts
|
|
||||||
|
|
||||||
# Check if port 5000 is occupied and free it
|
# Check if port 5000 is occupied and free it
|
||||||
if lsof -i :5000 &> /dev/null; then
|
if lsof -i :5000 &> /dev/null; then
|
||||||
PORT_PID=$(lsof -t -i :5000)
|
PORT_PID=$(lsof -t -i :5000)
|
||||||
|
|||||||
@ -34,4 +34,4 @@ fi
|
|||||||
|
|
||||||
# 3. Start Vite Dev Server
|
# 3. Start Vite Dev Server
|
||||||
info "Starting Vite frontend dev server..."
|
info "Starting Vite frontend dev server..."
|
||||||
npm run dev -- --host
|
npm run dev
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user