diff --git a/app/(app)/search/page.tsx b/app/(app)/search/page.tsx index 0d35062..cdf0fb1 100644 --- a/app/(app)/search/page.tsx +++ b/app/(app)/search/page.tsx @@ -4,6 +4,7 @@ import { useState, useTransition, useCallback } from 'react'; import { SearchInput } from '@/components/search/SearchInput'; import { ResultsTable } from '@/components/search/ResultsTable'; import { EmptyState } from '@/components/search/EmptyState'; +import { SyncButton } from '@/components/search/SyncButton'; import { searchPolicySettings, seedMyTenantData, @@ -78,10 +79,15 @@ export default function SearchPage() {
- Global Policy Search - - Search across all your Intune policy settings by keyword - +
+
+ Global Policy Search + + Search across all your Intune policy settings by keyword + +
+ +
diff --git a/components/search/SyncButton.tsx b/components/search/SyncButton.tsx new file mode 100644 index 0000000..4f6ee5d --- /dev/null +++ b/components/search/SyncButton.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { Button } from '@/components/ui/button'; +import { RefreshCw } from 'lucide-react'; +import { triggerPolicySync } from '@/lib/actions/policySettings'; +import { toast } from 'sonner'; + +export function SyncButton() { + const [isPending, startTransition] = useTransition(); + + const handleSync = () => { + startTransition(async () => { + try { + const result = await triggerPolicySync(); + + if (result.success) { + toast.success(result.message ?? 'Policy sync triggered successfully'); + } else { + toast.error(result.error ?? 'Failed to trigger sync'); + } + } catch (error) { + toast.error('An unexpected error occurred'); + } + }); + }; + + return ( + + ); +} diff --git a/lib/actions/policySettings.ts b/lib/actions/policySettings.ts index 8680e34..cb46fbe 100644 --- a/lib/actions/policySettings.ts +++ b/lib/actions/policySettings.ts @@ -3,6 +3,7 @@ import { db, policySettings, type PolicySetting } from '@/lib/db'; import { getUserAuth } from '@/lib/auth/utils'; import { eq, ilike, or, desc, and } from 'drizzle-orm'; +import { env } from '@/lib/env.mjs'; export interface PolicySettingSearchResult { id: string; @@ -288,3 +289,61 @@ export async function seedMyTenantData(): Promise<{ return { success: false, error: 'Failed to seed data' }; } } + +/** + * Trigger manual policy sync via n8n webhook + * + * **Security**: This function enforces tenant isolation by: + * 1. Validating user session via getUserAuth() + * 2. Extracting tenantId from session + * 3. Sending only the authenticated user's tenantId to n8n + * + * @returns Success/error result + */ +export async function triggerPolicySync(): Promise<{ success: boolean; message?: string; error?: string }> { + try { + const { session } = await getUserAuth(); + + if (!session?.user) { + return { success: false, error: 'Not authenticated' }; + } + + const tenantId = session.user.tenantId; + if (!tenantId) { + return { success: false, error: 'No tenant ID found in session' }; + } + + const webhookUrl = env.N8N_SYNC_WEBHOOK_URL; + if (!webhookUrl) { + return { success: false, error: 'Sync webhook not configured' }; + } + + // Trigger n8n workflow + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + tenantId, + source: 'manual_trigger', + triggeredAt: new Date().toISOString(), + }), + }); + + if (!response.ok) { + throw new Error(`Webhook responded with status ${response.status}`); + } + + return { + success: true, + message: 'Policy sync triggered successfully', + }; + } catch (error) { + console.error('Failed to trigger policy sync:', error); + return { + success: false, + error: 'Failed to trigger sync. Please try again later.', + }; + } +} diff --git a/lib/env.mjs b/lib/env.mjs index 7e013ae..750f2bc 100644 --- a/lib/env.mjs +++ b/lib/env.mjs @@ -23,6 +23,9 @@ export const env = createEnv({ // Policy Settings Ingestion API POLICY_API_SECRET: z.string().optional(), + + // n8n Webhook for manual policy sync + N8N_SYNC_WEBHOOK_URL: z.string().optional(), }, client: { NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(), diff --git a/specs/002-manual-policy-sync/checklists/requirements.md b/specs/002-manual-policy-sync/checklists/requirements.md new file mode 100644 index 0000000..339edf5 --- /dev/null +++ b/specs/002-manual-policy-sync/checklists/requirements.md @@ -0,0 +1,43 @@ +# Specification Quality Checklist: Manual Policy Sync Button + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-12-06 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +All checklist items passed. Specification is ready for planning phase (`/speckit.plan`). + +**Key Strengths**: +- Clear prioritization (P1, P2, P3) of user stories +- Comprehensive edge case coverage +- Well-defined success criteria with measurable metrics +- Technology-agnostic requirements +- Clear scope boundaries (Out of Scope section) + +**Ready for**: `/speckit.plan` to generate implementation plan diff --git a/specs/002-manual-policy-sync/spec.md b/specs/002-manual-policy-sync/spec.md new file mode 100644 index 0000000..96f4076 --- /dev/null +++ b/specs/002-manual-policy-sync/spec.md @@ -0,0 +1,120 @@ +# Feature Specification: Manual Policy Sync Button + +**Feature Branch**: `002-manual-policy-sync` +**Created**: 2025-12-06 +**Status**: Draft +**Input**: User description: "Manual Policy Sync Button - Allow admins to trigger immediate policy synchronization from Intune without waiting for scheduled sync" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Trigger Immediate Sync (Priority: P1) + +An IT administrator has just made changes to an Intune policy and wants to see those changes reflected in TenantPilot immediately without waiting for the scheduled nightly sync. + +**Why this priority**: Core functionality that directly addresses the pain point of waiting for scheduled syncs. Delivers immediate value and is the primary reason for this feature. + +**Independent Test**: Admin can click "Sync Policies" button on search page, receives confirmation that sync started, and n8n workflow is triggered for their tenant. + +**Acceptance Scenarios**: + +1. **Given** an authenticated admin is on the `/search` page, **When** they click the "Sync Policies" button, **Then** the button shows a loading state with "Syncing..." text and a spinner +2. **Given** a sync request is successfully sent to n8n, **When** the request completes, **Then** a success toast message appears saying "Policy sync triggered successfully" +3. **Given** an authenticated admin with a valid tenant ID, **When** they trigger a sync, **Then** the n8n webhook receives a POST request with their tenant ID and source "manual_trigger" + +--- + +### User Story 2 - Handle Sync Errors Gracefully (Priority: P2) + +When the sync cannot be triggered (network error, webhook down, etc.), the admin needs clear feedback about what went wrong. + +**Why this priority**: Essential for production reliability. Without error handling, users won't know if sync failed and may think it succeeded. + +**Independent Test**: Can be tested by simulating webhook failures (wrong URL, network timeout) and verifying error messages appear. + +**Acceptance Scenarios**: + +1. **Given** the n8n webhook is unreachable, **When** admin clicks "Sync Policies", **Then** an error toast appears with message "Failed to trigger sync. Please try again later." +2. **Given** a sync request fails, **When** the error occurs, **Then** the button returns to its normal state (not stuck in loading) +3. **Given** user is not authenticated, **When** they attempt to trigger sync, **Then** they receive "Not authenticated" error + +--- + +### User Story 3 - Prevent Multiple Simultaneous Syncs (Priority: P3) + +Admin should not be able to trigger multiple syncs simultaneously for the same tenant to avoid duplicate processing. + +**Why this priority**: Nice-to-have for UX polish. Prevents confusion but not critical for MVP since n8n workflows can handle duplicates. + +**Independent Test**: Click sync button multiple times rapidly and verify only one request is sent, button stays disabled during processing. + +**Acceptance Scenarios**: + +1. **Given** a sync is in progress, **When** admin clicks the button again, **Then** the button remains disabled and no additional request is sent +2. **Given** a sync completes (success or error), **When** the toast dismisses, **Then** the button becomes clickable again + +--- + +### Edge Cases + +- What happens when user's session has no tenant ID? (Show error: "No tenant ID found in session") +- What happens when N8N_SYNC_WEBHOOK_URL environment variable is not configured? (Show error: "Sync webhook not configured") +- What happens when n8n webhook returns non-200 status? (Log error, show user-friendly message) +- What happens if user navigates away while sync is in progress? (No issue - webhook call already sent, no cleanup needed) + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST provide a "Sync Policies" button on the `/search` page visible to authenticated users +- **FR-002**: System MUST validate user authentication before allowing sync trigger +- **FR-003**: System MUST extract tenant ID from user session and include it in webhook payload +- **FR-004**: System MUST send POST request to n8n webhook with JSON body containing `{ tenantId, source: "manual_trigger", triggeredAt: ISO timestamp }` +- **FR-005**: System MUST show loading state (spinner + "Syncing..." text) while request is in progress +- **FR-006**: System MUST display success toast notification when sync is triggered successfully +- **FR-007**: System MUST display error toast notification when sync fails with user-friendly error message +- **FR-008**: System MUST disable button during sync operation to prevent duplicate requests +- **FR-009**: System MUST log sync errors to console for debugging +- **FR-010**: System MUST validate that N8N_SYNC_WEBHOOK_URL environment variable is configured before attempting request + +### Key Entities *(include if feature involves data)* + +- **Sync Request**: Represents a manual sync trigger event + - Attributes: tenantId (from session), source ("manual_trigger"), triggeredAt (ISO timestamp) + - Sent to n8n webhook, not persisted in database + - Used for audit trail in n8n workflow logs + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Admin can trigger policy sync with single button click from search page +- **SC-002**: Sync request reaches n8n webhook within 2 seconds of button click +- **SC-003**: Admin receives visual feedback (loading state + toast) within 1 second of clicking button +- **SC-004**: Error scenarios (network failure, missing config) show clear error messages to user +- **SC-005**: Button correctly handles rapid multiple clicks without sending duplicate requests +- **SC-006**: 100% of sync triggers include correct tenant ID from authenticated user's session + +## Assumptions + +- N8N_SYNC_WEBHOOK_URL will be configured in environment variables before feature deployment +- n8n workflow already exists and can handle manual sync requests (separate from scheduled sync) +- Webhook endpoint accepts POST requests with JSON body +- Tenant ID is always available in authenticated user's session (handled by existing auth logic) +- No database changes required - this is a client-side UI + server action feature +- Sync is asynchronous - user doesn't wait for actual Intune data fetch to complete + +## Dependencies + +- Existing authentication system (NextAuth with tenant ID in session) +- Existing server actions infrastructure (`lib/actions/policySettings.ts`) +- Existing UI components (Shadcn Button, Toast from sonner) +- n8n webhook endpoint must be deployed and accessible +- Environment variable validation system (`lib/env.mjs`) + +## Out of Scope + +- Showing sync progress or status (user only gets confirmation that sync was triggered) +- Displaying last sync timestamp on the page +- Limiting sync frequency (e.g., rate limiting to once per 5 minutes) +- Syncing specific policies (all policies for tenant are synced) +- Admin controls for other tenants (admin can only sync their own tenant)