1import { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
3import MainLayout from '../components/Layout/MainLayout';
4import './Integrations.css';
6function Integrations() {
7 // Exchange integration state
8 const [exchangeEmail, setExchangeEmail] = useState('');
9 const [exchangeStatus, setExchangeStatus] = useState('');
10 const [exchangeLoading, setExchangeLoading] = useState(false);
11 const [tenantExchangeEmail, setTenantExchangeEmail] = useState('');
13 // Simulate login (replace with real OAuth flow)
14 const handleExchangeLogin = async () => {
15 setExchangeLoading(true);
16 setExchangeStatus('');
18 // TODO: Replace with backend OAuth/Graph login
19 await new Promise(res => setTimeout(res, 1200));
20 setTenantExchangeEmail(exchangeEmail);
21 setExchangeStatus('Connected to Exchange as ' + exchangeEmail);
23 setExchangeStatus('Login failed: ' + err.message);
25 setExchangeLoading(false);
27 // selectedTenantId should come from props/context, not localStorage
28 const [selectedTenantId, setSelectedTenantId] = useState('');
29 const [integrations, setIntegrations] = useState([]);
30 const [loading, setLoading] = useState(true);
31 const [error, setError] = useState(null);
32 const [configuring, setConfiguring] = useState(null); // integration being configured
33 const [configForm, setConfigForm] = useState({});
34 const [tenantMap, setTenantMap] = useState({});
35 const [meshcentralEnabled, setMeshcentralEnabled] = useState(true);
36 const [meshcentralLoading, setMeshcentralLoading] = useState(false);
37 // No need for separate root detection, handled by TenantSelector
39 // Stripe Connect state
40 const [stripeConfig, setStripeConfig] = useState(null);
41 const [stripeLoading, setStripeLoading] = useState(false);
42 const [stripeError, setStripeError] = useState(null);
44 // Integration categories and definitions
45 const integrationGroups = {
50 description: 'Open-source web-based remote management and desktop control. Supports RDP, VNC, terminal, and file transfer with browser-based access.',
52 iconBg: 'linear-gradient(135deg, #0078d4 0%, #005ba3 100%)',
53 details: 'MeshCentral provides full remote control, file transfer, and terminal access directly in your browser.',
54 serverInfo: 'mesh.everydaytech.au:4430'
61 description: 'Send real-time notifications and ticket updates to Slack channels. Keep your team informed instantly.',
65 { key: 'webhook_url', label: 'Webhook URL', type: 'text', required: true },
66 { key: 'default_channel', label: 'Default Channel', type: 'text', required: false },
67 { key: 'notify_on_ticket_create', label: 'Notify on Ticket Create', type: 'checkbox', required: false }
73 description: 'Powerful email delivery service for sending ticket notifications, alerts, and transactional emails reliably.',
77 { key: 'api_key', label: 'API Key', type: 'password', required: true },
78 { key: 'domain', label: 'Domain', type: 'text', required: true },
79 { key: 'from_email', label: 'From Email', type: 'email', required: true },
80 { key: 'from_name', label: 'From Name', type: 'text', required: false }
88 description: 'Seamlessly sync invoices, payments, and financial data with Xero accounting software. Automated bookkeeping.',
92 { key: 'client_id', label: 'Client ID', type: 'text', required: true },
93 { key: 'client_secret', label: 'Client Secret', type: 'password', required: true },
94 { key: 'tenant_id', label: 'Xero Tenant ID', type: 'text', required: true },
95 { key: 'auto_sync_invoices', label: 'Auto-sync Invoices', type: 'checkbox', required: false }
102 name: 'Stripe Connect',
103 description: 'Accept payments, manage subscriptions, and automate billing with Stripe. Secure payment processing with multi-tenant isolation.',
105 iconBg: 'linear-gradient(135deg, #635BFF 0%, #0073E6 100%)',
106 details: 'Connect your Stripe account to enable payment processing, recurring billing, and invoice payments.',
107 requiresOAuth: true // Special handling for OAuth flow
112 const getTenantHeaders = async (baseHeaders = {}) => {
113 const headers = { ...baseHeaders };
114 const token = localStorage.getItem('token');
116 if (!token) return headers;
118 // Parse token to check if user is MSP
120 const payload = JSON.parse(atob(token.split('.')[1]));
121 const isMSP = payload.role === 'msp' || payload.role === 'admin';
123 if (isMSP && selectedTenantId) {
124 // Fetch tenant subdomain
125 const tenantRes = await apiFetch('/tenants', {
126 headers: { Authorization: `Bearer ${token}`, 'X-Tenant-Subdomain': 'admin' }
129 const data = await tenantRes.json();
130 const tenants = Array.isArray(data) ? data : (data.tenants || []);
131 const tenant = tenants.find(t => String(t.tenant_id) === String(selectedTenantId));
133 headers['X-Tenant-Subdomain'] = tenant.subdomain;
138 console.error('Error parsing token:', err);
144 const fetchIntegrations = async () => {
147 const token = localStorage.getItem('token');
148 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
150 const res = await apiFetch('/integrations');
151 if (!res.ok) throw new Error('Failed to load integrations');
153 const data = await res.json();
154 setIntegrations(data);
156 // Check if MeshCentral is enabled
157 const meshcentral = data.find(i => i.integration_type === 'meshcentral');
158 setMeshcentralEnabled(meshcentral?.is_active ?? true); // Default to true
162 setError(err.message || 'Error loading integrations');
171 }, [selectedTenantId]);
173 // Fetch Stripe Connect configuration
174 const fetchStripeConfig = async () => {
176 const token = localStorage.getItem('token');
177 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
179 const res = await apiFetch('/stripe/config', {
184 if (res.status === 404) {
185 setStripeConfig({ configured: false });
188 throw new Error('Failed to fetch Stripe configuration');
191 const data = await res.json();
192 setStripeConfig(data);
194 console.error('Error fetching Stripe config:', err);
195 setStripeConfig({ configured: false });
199 // Start Stripe Connect onboarding
200 const handleStripeConnect = async () => {
201 setStripeLoading(true);
202 setStripeError(null);
205 const token = localStorage.getItem('token');
206 const headers = await getTenantHeaders({
207 Authorization: `Bearer ${token}`,
208 'Content-Type': 'application/json'
211 // Get user email from token
212 const payload = JSON.parse(atob(token.split('.')[1]));
213 const email = payload.email || 'billing@example.com';
215 const res = await apiFetch('/stripe/connect/start', {
217 headers: { 'Content-Type': 'application/json' },
218 body: JSON.stringify({
225 const error = await res.json();
226 throw new Error(error.message || 'Failed to start Stripe onboarding');
229 const data = await res.json();
231 // Redirect to Stripe onboarding
232 window.location.href = data.onboarding_url;
234 console.error('Stripe Connect error:', err);
235 setStripeError(err.message);
237 setStripeLoading(false);
241 // Sync Stripe account status
242 const handleStripeSyncStatus = async () => {
243 setStripeLoading(true);
244 setStripeError(null);
247 const token = localStorage.getItem('token');
248 const headers = await getTenantHeaders({
249 Authorization: `Bearer ${token}`,
250 'Content-Type': 'application/json'
253 const res = await apiFetch('/stripe/connect/sync', {
255 headers: { 'Content-Type': 'application/json' }
259 throw new Error('Failed to sync Stripe account status');
262 // Refresh config after sync
263 await fetchStripeConfig();
265 console.error('Stripe sync error:', err);
266 setStripeError(err.message);
268 setStripeLoading(false);
272 // Disconnect Stripe account
273 const handleStripeDisconnect = async () => {
274 if (!confirm('Are you sure you want to disconnect your Stripe account? This will disable payment processing.')) {
278 setStripeLoading(true);
279 setStripeError(null);
282 const token = localStorage.getItem('token');
283 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
285 const res = await apiFetch('/stripe/connect', {
290 throw new Error('Failed to disconnect Stripe account');
294 await fetchStripeConfig();
296 console.error('Stripe disconnect error:', err);
297 setStripeError(err.message);
299 setStripeLoading(false);
303 // Check for Stripe onboarding return
305 const urlParams = new URLSearchParams(window.location.search);
306 const onboardingStatus = urlParams.get('stripe_onboarding');
308 if (onboardingStatus === 'success') {
309 // Sync account status after successful onboarding
310 handleStripeSyncStatus();
312 window.history.replaceState({}, document.title, window.location.pathname);
313 } else if (onboardingStatus === 'refresh') {
314 // Link expired - show message
315 setStripeError('Onboarding link expired. Please try connecting again.');
316 window.history.replaceState({}, document.title, window.location.pathname);
322 }, [selectedTenantId]);
324 const handleConfigure = (integration) => {
325 const existing = integrations.find(i => i.integration_type === integration.type);
329 setConfigForm(existing.config || {});
331 // New integration - initialize with defaults
333 integration.configFields.forEach(field => {
334 defaults[field.key] = field.type === 'checkbox' ? false : '';
336 setConfigForm(defaults);
339 setConfiguring(integration);
342 const handleSaveIntegration = async () => {
344 const token = localStorage.getItem('token');
345 const headers = await getTenantHeaders({
346 Authorization: `Bearer ${token}`,
347 'Content-Type': 'application/json'
350 const existing = integrations.find(i => i.integration_type === configuring.type);
351 const method = existing ? 'PUT' : 'POST';
353 ? `/integrations/${existing.integration_id}`
357 integration_type: configuring.type,
362 const res = await apiFetch(url, {
364 headers: { 'Content-Type': 'application/json' },
365 body: JSON.stringify(body)
368 if (!res.ok) throw new Error('Failed to save integration');
370 await fetchIntegrations();
371 setConfiguring(null);
374 alert(err.message || 'Error saving integration');
378 const handleToggleIntegration = async (integration) => {
380 const token = localStorage.getItem('token');
381 const headers = await getTenantHeaders({
382 Authorization: `Bearer ${token}`,
383 'Content-Type': 'application/json'
386 const res = await apiFetch(`/integrations/${integration.integration_id}`, {
388 headers: { 'Content-Type': 'application/json' },
389 body: JSON.stringify({
391 is_active: !integration.is_active
395 if (!res.ok) throw new Error('Failed to update integration');
397 await fetchIntegrations();
399 alert(err.message || 'Error updating integration');
403 const handleDeleteIntegration = async (integration) => {
404 if (!confirm(`Are you sure you want to delete the ${integration.integration_type} integration?`)) {
409 const token = localStorage.getItem('token');
410 const headers = await getTenantHeaders({ Authorization: `Bearer ${token}` });
412 const res = await apiFetch(`/integrations/${integration.integration_id}`, {
416 if (!res.ok) throw new Error('Failed to delete integration');
418 await fetchIntegrations();
420 alert(err.message || 'Error deleting integration');
424 const isIntegrationConfigured = (type) => {
425 return integrations.some(i => i.integration_type === type);
428 const getIntegrationStatus = (type) => {
429 const integration = integrations.find(i => i.integration_type === type);
430 return integration?.is_active ? 'active' : 'inactive';
433 // Azure AD SSO integration state
434 const [azureStatus, setAzureStatus] = useState('');
435 const [azureLoading, setAzureLoading] = useState(false);
436 const [azureUser, setAzureUser] = useState(null);
438 const handleAzureStart = async () => {
439 setAzureLoading(true);
442 // Get Azure AD OAuth2 URL from backend
443 const res = await apiFetch(`/sso/azure/${selectedTenantId}/start`);
444 if (!res.ok) throw new Error('Failed to get Azure AD login URL');
445 const data = await res.json();
446 window.location.href = data.url; // Redirect to Azure AD login
448 setAzureStatus('Azure AD SSO setup failed: ' + err.message);
450 setAzureLoading(false);
453 // MeshCentral toggle handler
454 const handleMeshcentralToggle = async (enabled) => {
455 setMeshcentralLoading(true);
457 const response = await apiFetch('/integrations/meshcentral', {
458 method: enabled ? 'POST' : 'DELETE',
459 headers: { 'Content-Type': 'application/json' },
460 body: JSON.stringify({ enabled })
464 throw new Error('Failed to update MeshCentral integration');
467 setMeshcentralEnabled(enabled);
469 console.error('MeshCentral toggle error:', err);
470 alert('Failed to update MeshCentral integration: ' + err.message);
472 setMeshcentralLoading(false);
478 <div className="integrations-page">
479 <div className="page-header">
480 <h1>Integrations</h1>
483 {error && <div className="error-message">{error}</div>}
486 <div className="loading">Loading integrations...</div>
489 {Object.entries(integrationGroups).map(([groupName, groupIntegrations]) => (
490 <div key={groupName}>
491 <h2 className="section-title">{groupName}</h2>
493 {groupIntegrations.map((integration) => {
494 // Special handling for Stripe Connect (OAuth flow)
495 if (integration.type === 'stripe') {
496 const isConnected = stripeConfig?.configured && stripeConfig?.charges_enabled;
497 const isVerified = stripeConfig?.account_status === 'verified';
498 const isPending = stripeConfig?.configured && !stripeConfig?.charges_enabled;
501 <div key={integration.type} className="remote-desktop-card">
502 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
503 <div style={{ flex: 1 }}>
504 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
509 background: integration.iconBg,
511 alignItems: 'center',
512 justifyContent: 'center',
518 <h3 style={{ margin: 0, marginBottom: '4px' }}>{integration.name}</h3>
519 <div className={`status-badge ${isConnected ? 'enabled' : 'disabled'}`}>
520 {isConnected ? (isVerified ? 'VERIFIED' : 'CONNECTED') : (isPending ? 'PENDING' : 'NOT CONNECTED')}
525 <p className="description">{integration.description}</p>
528 <div style={{ color: '#dc3545', fontSize: '14px', marginTop: '8px', padding: '8px', background: '#ffe6e6', borderRadius: '4px' }}>
534 <div className="config-details" style={{ marginTop: '12px' }}>
535 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '8px' }}>
537 <strong>Account ID:</strong>
538 <code style={{ fontSize: '11px', display: 'block', marginTop: '4px' }}>
539 {stripeConfig.stripe_account_id}
543 <strong>Country:</strong> {stripeConfig.country || 'US'}
547 <div style={{ display: 'flex', gap: '16px', marginTop: '8px' }}>
548 <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
549 <span style={{ fontSize: '18px' }}>
550 {stripeConfig.charges_enabled ? 'â
' : 'â'}
552 <span style={{ fontSize: '13px' }}>Charges</span>
554 <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
555 <span style={{ fontSize: '18px' }}>
556 {stripeConfig.payouts_enabled ? 'â
' : 'â'}
558 <span style={{ fontSize: '13px' }}>Payouts</span>
562 {stripeConfig.requirements_currently_due?.length > 0 && (
563 <div style={{ marginTop: '12px', padding: '8px', background: '#fff3cd', borderRadius: '4px', fontSize: '13px' }}>
564 â ī¸ <strong>Action Required:</strong> {stripeConfig.requirements_currently_due.length} verification step(s) needed
568 {stripeConfig.onboarding_completed_at && (
569 <div style={{ marginTop: '8px', fontSize: '12px', color: 'var(--text-secondary)' }}>
570 Connected on {new Date(stripeConfig.onboarding_completed_at).toLocaleDateString()}
577 <div style={{ marginLeft: '24px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
580 className="btn-primary"
581 onClick={handleStripeConnect}
582 disabled={stripeLoading}
583 style={{ whiteSpace: 'nowrap' }}
585 {stripeLoading ? 'Connecting...' : 'đ Connect with Stripe'}
590 className="btn-secondary"
591 onClick={handleStripeSyncStatus}
592 disabled={stripeLoading}
593 style={{ whiteSpace: 'nowrap', fontSize: '13px' }}
595 {stripeLoading ? 'Syncing...' : 'đ Sync Status'}
598 className="btn-secondary"
599 onClick={handleStripeDisconnect}
600 disabled={stripeLoading}
601 style={{ whiteSpace: 'nowrap', fontSize: '13px', color: '#dc3545' }}
613 // Standard integration handling (toggle switch)
614 const configured = integrations.find(i => i.integration_type === integration.type);
615 const isEnabled = integration.type === 'meshcentral' ? meshcentralEnabled : configured?.is_active || false;
616 const isLoading = integration.type === 'meshcentral' && meshcentralLoading;
619 <div key={integration.type} className="remote-desktop-card">
620 <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
621 <div style={{ flex: 1 }}>
622 <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
627 background: integration.iconBg,
629 alignItems: 'center',
630 justifyContent: 'center',
636 <h3 style={{ margin: 0, marginBottom: '4px' }}>{integration.name}</h3>
637 <div className={`status-badge ${isEnabled ? 'enabled' : 'disabled'}`}>
638 {isEnabled ? 'ENABLED' : 'DISABLED'}
643 <p className="description">{integration.description}</p>
645 {isEnabled && integration.details && (
646 <div className="config-details">
647 {integration.serverInfo && (
648 <div style={{ marginBottom: '8px' }}>
649 <strong>Server:</strong>
650 <code>{integration.serverInfo}</code>
653 <div style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>
654 âšī¸ {integration.details}
660 <div style={{ marginLeft: '24px' }}>
661 <label className="switch">
666 if (integration.type === 'meshcentral') {
667 handleMeshcentralToggle(e.target.checked);
669 handleToggleIntegration(integration.type, e.target.checked);
674 <span className="slider"></span>
686 {/* Configuration Modal */}
688 <div className="modal-overlay" onClick={() => setConfiguring(null)}>
689 <div className="modal-content" onClick={(e) => e.stopPropagation()}>
690 <div className="modal-header">
691 <h2>Configure {configuring.name}</h2>
692 <button className="close-btn" onClick={() => setConfiguring(null)}>Ã</button>
695 <div className="modal-body">
696 <p className="integration-description">{configuring.description}</p>
698 <form className="integration-form">
699 {configuring.configFields?.map((field) => (
700 <div key={field.key} className="form-group">
701 <label htmlFor={field.key}>
703 {field.required && <span className="required">*</span>}
706 {field.type === 'checkbox' ? (
710 checked={configForm[field.key] || false}
711 onChange={(e) => setConfigForm({ ...configForm, [field.key]: e.target.checked })}
717 value={configForm[field.key] || ''}
718 onChange={(e) => setConfigForm({ ...configForm, [field.key]: e.target.value })}
719 required={field.required}
720 placeholder={field.label}
728 <div className="modal-footer">
729 <button onClick={() => setConfiguring(null)} className="btn-secondary">
732 <button onClick={handleSaveIntegration} className="btn-primary">
743export default Integrations;