diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx index ed9631250..8e786a0b0 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/Employee.tsx @@ -16,7 +16,6 @@ interface EmployeeDetailsProps { })[]; fleetPolicies: FleetPolicy[]; host: Host; - isFleetEnabled: boolean; } export function Employee({ @@ -25,7 +24,6 @@ export function Employee({ trainingVideos, fleetPolicies, host, - isFleetEnabled, }: EmployeeDetailsProps) { return (
@@ -36,7 +34,6 @@ export function Employee({ trainingVideos={trainingVideos} fleetPolicies={fleetPolicies} host={host} - isFleetEnabled={isFleetEnabled} />
); diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx index 35a938e3f..4a0ecb92b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/components/EmployeeTasks.tsx @@ -13,7 +13,6 @@ export const EmployeeTasks = ({ trainingVideos, host, fleetPolicies, - isFleetEnabled, }: { employee: Member & { user: User; @@ -24,7 +23,6 @@ export const EmployeeTasks = ({ })[]; host: Host; fleetPolicies: FleetPolicy[]; - isFleetEnabled: boolean; }) => { return ( @@ -43,7 +41,7 @@ export const EmployeeTasks = ({ Policies Training Videos - {isFleetEnabled && Device} + Device @@ -116,45 +114,43 @@ export const EmployeeTasks = ({ - {isFleetEnabled && ( - - {host ? ( - - - {host.computer_name}'s Policies - - - {fleetPolicies.map((policy) => ( -
-

{policy.name}

- {policy.response === 'pass' ? ( -
- - Pass -
- ) : ( -
- - Fail -
- )} -
- ))} -
-
- ) : ( -
-

No device found.

-
- )} -
- )} + + {host ? ( + + + {host.computer_name}'s Policies + + + {fleetPolicies.map((policy) => ( +
+

{policy.name}

+ {policy.response === 'pass' ? ( +
+ + Pass +
+ ) : ( +
+ + Fail +
+ )} +
+ ))} +
+
+ ) : ( +
+

No device found.

+
+ )} +
diff --git a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx index 61e870d0d..859ad22fc 100644 --- a/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/[employeeId]/page.tsx @@ -1,6 +1,5 @@ import { auth } from '@/utils/auth'; -import { getPostHogClient } from '@/app/posthog'; import { type TrainingVideo, trainingVideos as trainingVideosData, @@ -40,10 +39,6 @@ export default async function EmployeeDetailsPage({ } const { fleetPolicies, device } = await getFleetPolicies(employee); - const isFleetEnabled = await getPostHogClient()?.isFeatureEnabled( - 'is-fleet-enabled', - session?.session.userId, - ); return ( ); } diff --git a/apps/app/src/app/(app)/[orgId]/people/layout.tsx b/apps/app/src/app/(app)/[orgId]/people/layout.tsx index f0aa85263..2efccc86b 100644 --- a/apps/app/src/app/(app)/[orgId]/people/layout.tsx +++ b/apps/app/src/app/(app)/[orgId]/people/layout.tsx @@ -1,4 +1,3 @@ -import { getPostHogClient } from '@/app/posthog'; import { auth } from '@/utils/auth'; import { SecondaryMenu } from '@comp/ui/secondary-menu'; import { db } from '@db'; @@ -27,11 +26,6 @@ export default async function Layout({ children }: { children: React.ReactNode } return roles.includes('employee'); }); - const isFleetEnabled = await getPostHogClient()?.isFeatureEnabled( - 'is-fleet-enabled', - session?.session.userId, - ); - return (
diff --git a/apps/app/src/jobs/tasks/device/create-fleet-label-for-org.ts b/apps/app/src/jobs/tasks/device/create-fleet-label-for-org.ts index deb595fae..73548ef27 100644 --- a/apps/app/src/jobs/tasks/device/create-fleet-label-for-org.ts +++ b/apps/app/src/jobs/tasks/device/create-fleet-label-for-org.ts @@ -29,20 +29,21 @@ export const createFleetLabelForOrg = task({ return; } - const fleetDevicePathMac = process.env.FLEET_DEVICE_PATH_MAC; - const fleetDevicePathWindows = process.env.FLEET_DEVICE_PATH_WINDOWS; + const fleetDevicePathMac = '/Users/Shared/.fleet'; + const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet'; + const windowsFallbackDir = 'C:\\Users\\Public\\CompAI\\Fleet'; - if (!fleetDevicePathMac || !fleetDevicePathWindows) { - logger.error('FLEET_DEVICE_PATH_MAC or FLEET_DEVICE_PATH_WINDOWS not configured'); - return; - } - - // Create a query that matches devices from either macOS or Windows - const query = `SELECT 1 FROM file WHERE path = '${fleetDevicePathMac}/${organizationId}' OR path = '${fleetDevicePathWindows}\\${organizationId}' LIMIT 1;`; + // Simple union query: only file table, constrained per platform path + const query = ` + SELECT 1 FROM file WHERE path = '${fleetDevicePathMac}/${organizationId}' + UNION SELECT 1 FROM file WHERE path = '${fleetDevicePathWindows}\\${organizationId}' + UNION SELECT 1 FROM file WHERE path = '${windowsFallbackDir}\\${organizationId}' + LIMIT 1;`; + const normalizedQuery = query.replace(/\s+/g, ' ').trim(); logger.info('Creating label', { name: organization.id, - query: query, + query: normalizedQuery, }); const fleet = await getFleetInstance(); @@ -53,7 +54,7 @@ export const createFleetLabelForOrg = task({ // Create a manual label that we can assign to hosts. labelResponse = await fleet.post('/labels', { name: organization.id, - query: query, + query: normalizedQuery, }); logger.info('Label created', { diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/EmployeeTasksList.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/EmployeeTasksList.tsx index 69e5292da..4113cdcc2 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/EmployeeTasksList.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/EmployeeTasksList.tsx @@ -15,7 +15,6 @@ interface EmployeeTasksListProps { member: Member; fleetPolicies: FleetPolicy[]; host: Host | null; - isFleetEnabled: boolean; } export const EmployeeTasksList = ({ @@ -24,7 +23,6 @@ export const EmployeeTasksList = ({ member, fleetPolicies, host, - isFleetEnabled, }: EmployeeTasksListProps) => { // Check completion status const hasAcceptedPolicies = @@ -58,16 +56,12 @@ export const EmployeeTasksList = ({ title: 'Accept security policies', content: , }, - ...(isFleetEnabled - ? [ - { - title: 'Download and install Comp AI Device Agent', - content: ( - - ), - }, - ] - : []), + { + title: 'Download and install Comp AI Device Agent', + content: ( + + ), + }, { title: 'Complete general security awareness training', content: , diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx index a8a56080e..48feb21e8 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/components/OrganizationDashboard.tsx @@ -15,7 +15,6 @@ interface OrganizationDashboardProps { member: MemberWithUserOrg; // Pass the full member object for user info etc. fleetPolicies: FleetPolicy[]; host: Host | null; - isFleetEnabled: boolean; } export async function OrganizationDashboard({ @@ -23,7 +22,6 @@ export async function OrganizationDashboard({ member, fleetPolicies, host, - isFleetEnabled, }: OrganizationDashboardProps) { // Fetch policies specific to the selected organization const policies = await db.policy.findMany({ @@ -74,7 +72,6 @@ export async function OrganizationDashboard({ member={member} // Pass the member object down fleetPolicies={fleetPolicies} host={host} - isFleetEnabled={isFleetEnabled ?? false} /> ); } diff --git a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx index 91a7483d7..79b16971c 100644 --- a/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx +++ b/apps/portal/src/app/(app)/(home)/[orgId]/page.tsx @@ -1,7 +1,6 @@ 'use server'; import { auth } from '@/app/lib/auth'; -import { getPostHogClient } from '@/app/posthog'; import { getFleetInstance } from '@/utils/fleet'; import type { Member } from '@db'; import { db } from '@db'; @@ -45,26 +44,13 @@ export default async function OrganizationPage({ params }: { params: Promise<{ o return redirect('/'); // Or appropriate login/auth route } - // Check fleet feature flag first - let isFleetEnabled = false; - try { - const postHogClient = await getPostHogClient(); - isFleetEnabled = - (await postHogClient?.isFeatureEnabled('is-fleet-enabled', session?.user.id)) ?? false; - } catch (error) { - console.error('Error checking fleet feature flag:', error); - // Default to false if there's an error - } - // Only fetch fleet policies if fleet is enabled let fleetPolicies: FleetPolicy[] = []; let device: Host | null = null; - if (isFleetEnabled) { - const fleetData = await getFleetPolicies(member); - fleetPolicies = fleetData.fleetPolicies; - device = fleetData.device; - } + const fleetData = await getFleetPolicies(member); + fleetPolicies = fleetData.fleetPolicies; + device = fleetData.device; return ( ); } catch (error) { diff --git a/apps/portal/src/app/api/download-agent/fleet-label.ts b/apps/portal/src/app/api/download-agent/fleet-label.ts index 2ebaa546e..259143e31 100644 --- a/apps/portal/src/app/api/download-agent/fleet-label.ts +++ b/apps/portal/src/app/api/download-agent/fleet-label.ts @@ -24,16 +24,21 @@ export async function createFleetLabel({ const fleet = await getFleetInstance(); logger('Fleet instance obtained successfully'); - // Create platform-specific query + // OS-specific queries: mac uses file-only; Windows uses UNION with file and registry const query = os === 'macos' ? `SELECT 1 FROM file WHERE path = '${fleetDevicePathMac}/${employeeId}' LIMIT 1;` - : `SELECT 1 FROM file WHERE path = '${fleetDevicePathWindows}\\${employeeId}' LIMIT 1;`; + : `SELECT 1 FROM file WHERE path = '${fleetDevicePathWindows}\\${employeeId}' + UNION SELECT 1 FROM file WHERE path = 'C:\\Users\\Public\\CompAI\\Fleet\\${employeeId}' + LIMIT 1;`; + + // Normalize whitespace to a single line to avoid issues with newlines/tabs + const normalizedQuery = query.replace(/\s+/g, ' ').trim(); logger('Generated Fleet query for label creation', { employeeId, os, - query, + query: normalizedQuery, }); logger('Sending POST request to Fleet API to create label...', { @@ -41,13 +46,13 @@ export async function createFleetLabel({ endpoint: '/labels', requestBody: { name: employeeId, - query: query, + query: normalizedQuery, }, }); const response = await fleet.post('/labels', { name: employeeId, - query: query, + query: normalizedQuery, }); logger('Fleet API response received', { diff --git a/apps/portal/src/app/api/download-agent/route.ts b/apps/portal/src/app/api/download-agent/route.ts index 40e085e11..ccfafac24 100644 --- a/apps/portal/src/app/api/download-agent/route.ts +++ b/apps/portal/src/app/api/download-agent/route.ts @@ -13,6 +13,10 @@ import { getScriptFilename, } from './scripts'; +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + // GET handler for direct browser downloads using token export async function GET(req: NextRequest) { const searchParams = req.nextUrl.searchParams; @@ -39,12 +43,12 @@ export async function GET(req: NextRequest) { os: 'macos' | 'windows'; }; - // Check environment configuration - const fleetDevicePathMac = process.env.FLEET_DEVICE_PATH_MAC; - const fleetDevicePathWindows = process.env.FLEET_DEVICE_PATH_WINDOWS; + // Hardcoded device marker paths used by the setup scripts + const fleetDevicePathMac = '/Users/Shared/.fleet'; + const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet'; const fleetBucketName = process.env.FLEET_AGENT_BUCKET_NAME; - if (!fleetDevicePathMac || !fleetDevicePathWindows || !fleetBucketName) { + if (!fleetBucketName) { return new NextResponse('Server configuration error', { status: 500 }); } @@ -63,6 +67,21 @@ export async function GET(req: NextRequest) { // Pipe archive to passthrough archive.pipe(passThrough); + // Robust error handling for staging/prod reliability + archive.on('error', (err) => { + logger('archiver_error', { message: err?.message, stack: (err as Error)?.stack }); + passThrough.destroy(err as Error); + }); + archive.on('warning', (warn) => { + logger('archiver_warning', { message: (warn as Error)?.message }); + }); + passThrough.on('error', (err) => { + logger('download_stream_error', { + message: (err as Error)?.message, + stack: (err as Error)?.stack, + }); + }); + // Add script file const scriptFilename = getScriptFilename(os); archive.append(script, { name: scriptFilename, mode: 0o755 }); @@ -84,6 +103,13 @@ export async function GET(req: NextRequest) { if (s3Response.Body) { const s3Stream = s3Response.Body as Readable; + s3Stream.on('error', (err) => { + logger('s3_stream_error', { + message: (err as Error)?.message, + stack: (err as Error)?.stack, + }); + passThrough.destroy(err as Error); + }); archive.append(s3Stream, { name: packageFilename, store: true }); } @@ -99,6 +125,7 @@ export async function GET(req: NextRequest) { 'Content-Type': 'application/zip', 'Content-Disposition': `attachment; filename="compai-device-agent-${os}.zip"`, 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'X-Accel-Buffering': 'no', }, }); } catch (error) { diff --git a/apps/portal/src/app/api/download-agent/scripts.ts b/apps/portal/src/app/api/download-agent/scripts.ts index 11b25c9de..11e3d7c6b 100644 --- a/apps/portal/src/app/api/download-agent/scripts.ts +++ b/apps/portal/src/app/api/download-agent/scripts.ts @@ -1,73 +1,11 @@ -import type { ScriptConfig, SupportedOS } from './types'; - -export function generateMacScript(config: ScriptConfig): string { - const { orgId, employeeId, fleetDevicePath } = config; - - return `#!/bin/bash -# Create org marker for Fleet policies/labels -set -euo pipefail -ORG_ID="${orgId}" -EMPLOYEE_ID="${employeeId}" -FLEET_DIR="${fleetDevicePath}" -mkdir -p "$FLEET_DIR" -echo "$ORG_ID" > "$FLEET_DIR/${orgId}" -echo "$EMPLOYEE_ID" > "$FLEET_DIR/${employeeId}" -chmod 755 "$FLEET_DIR" -chmod 644 "$FLEET_DIR/${orgId}" -chmod 644 "$FLEET_DIR/${employeeId}" -exit 0`; -} - -export function generateWindowsScript(config: ScriptConfig): string { - const { orgId, employeeId, fleetDevicePath } = config; - - return `@echo off -REM Create org marker for Fleet policies/labels -setlocal -set ORG_ID=${orgId} -set EMPLOYEE_ID=${employeeId} -set FLEET_DIR=${fleetDevicePath} -if not exist "%FLEET_DIR%" mkdir "%FLEET_DIR%" -echo %ORG_ID% > "%FLEET_DIR%\\%ORG_ID%" -echo %EMPLOYEE_ID% > "%FLEET_DIR%\\%EMPLOYEE_ID%" -exit /b 0`; -} - -export function getScriptFilename(os: SupportedOS): string { - return os === 'macos' ? 'run_me_first.command' : 'run_me_first.bat'; -} - -export function getPackageFilename(os: SupportedOS): string { - return os === 'macos' ? 'compai-device-agent.pkg' : 'compai-device-agent.msi'; -} - -export function getReadmeContent(os: SupportedOS): string { - if (os === 'macos') { - return `Installation Instructions for macOS: - -1. First, run the setup script by double-clicking "run_me_first.command" - - This will create the necessary organization markers for device management - - You may need to allow the script to run in System Preferences > Security & Privacy - -2. Then, install the agent by double-clicking "compai-device-agent.pkg" - - Follow the installation wizard - - You may need to allow the installer in System Preferences > Security & Privacy - -3. The agent will start automatically after installation -`; - } - - return `Installation Instructions for Windows: - -1. First, run the setup script: - - Right-click on "run_me_first.bat" and select "Run as administrator" - - This will create the necessary organization markers for device management - -2. Then, install the agent: - - Double-click "compai-device-agent.msi" - - Follow the installation wizard - - Windows may show a security warning - click "More info" and then "Run anyway" - -3. The agent will start automatically after installation -`; -} +import { getPackageFilename, getReadmeContent, getScriptFilename } from './scripts/common'; +import { generateMacScript } from './scripts/mac'; +import { generateWindowsScript } from './scripts/windows'; + +export { + generateMacScript, + generateWindowsScript, + getPackageFilename, + getReadmeContent, + getScriptFilename, +}; diff --git a/apps/portal/src/app/api/download-agent/scripts/common.ts b/apps/portal/src/app/api/download-agent/scripts/common.ts new file mode 100644 index 000000000..dbe01eeac --- /dev/null +++ b/apps/portal/src/app/api/download-agent/scripts/common.ts @@ -0,0 +1,44 @@ +import type { SupportedOS } from '../types'; + +export function getScriptFilename(os: SupportedOS): string { + return os === 'macos' ? 'run_me_first.command' : 'run_me_first.bat'; +} + +export function getPackageFilename(os: SupportedOS): string { + return os === 'macos' ? 'compai-device-agent.pkg' : 'compai-device-agent.msi'; +} + +export function getReadmeContent(os: SupportedOS): string { + if (os === 'macos') { + return `Installation Instructions for macOS: + +1. First, run the setup script by double-clicking "run_me_first.command" + - This will create the necessary organization markers for device management + - You may need to allow the script to run in System Preferences > Security & Privacy + +2. Then, install the agent by double-clicking "compai-device-agent.pkg" + - Follow the installation wizard + - You may need to allow the installer in System Preferences > Security & Privacy + +3. The agent will start automatically after installation +`; + } + + return `Installation Instructions for Windows: + +1. First, run the setup script: + - Right-click on "run_me_first.bat" and select "Run as administrator" (required) + - This writes organization markers to the device and registry + - If prompted by SmartScreen, click "More info" -> "Run anyway" + +2. Then, install the agent: + - Double-click "compai-device-agent.msi" and follow the wizard + +3. Troubleshooting: + - If setup fails, open the log at: %ProgramData%\\CompAI\\Fleet or %Public%\\CompAI\\Fleet -> setup.log + - Ensure your antivirus or endpoint protection allows running local .bat files + - If you cannot run as administrator, ask IT to assist or install both files and registry keys manually + +4. After installation, the agent will start automatically. +`; +} diff --git a/apps/portal/src/app/api/download-agent/scripts/index.ts b/apps/portal/src/app/api/download-agent/scripts/index.ts new file mode 100644 index 000000000..613fb7274 --- /dev/null +++ b/apps/portal/src/app/api/download-agent/scripts/index.ts @@ -0,0 +1,3 @@ +export { getPackageFilename, getReadmeContent, getScriptFilename } from './common'; +export { generateMacScript } from './mac'; +export { generateWindowsScript } from './windows'; diff --git a/apps/portal/src/app/api/download-agent/scripts/mac.ts b/apps/portal/src/app/api/download-agent/scripts/mac.ts new file mode 100644 index 000000000..d51c854f2 --- /dev/null +++ b/apps/portal/src/app/api/download-agent/scripts/mac.ts @@ -0,0 +1,120 @@ +import type { ScriptConfig } from '../types'; + +export function generateMacScript(config: ScriptConfig): string { + const { orgId, employeeId, fleetDevicePath } = config; + + return `#!/bin/bash +# CompAI Device Setup (macOS) +# Creates organization markers for Fleet policies/labels with clear, human-readable output + +set -uo pipefail + +ORG_ID="${orgId}" +EMPLOYEE_ID="${employeeId}" +FLEET_DIR="${fleetDevicePath}" +LOG_FILE="/tmp/compai-setup.log" +HAS_ERROR=0 +ERROR_TEXT="" + +# Colors (ANSI escapes) +NC='\x1b[0m' +GREEN='\x1b[0;32m' +YELLOW='\x1b[0;33m' +RED='\x1b[0;31m' +BOLD='\x1b[1m' + +timestamp() { date '+%Y-%m-%d %H:%M:%S'; } +log_info() { printf "[%s] %bINFO%b %s\n" "$(timestamp)" "$GREEN" "$NC" "$1" | tee -a "$LOG_FILE"; } +log_warn() { printf "[%s] %bWARN%b %s\n" "$(timestamp)" "$YELLOW" "$NC" "$1" | tee -a "$LOG_FILE"; } +log_error() { printf "[%s] %bERROR%b %s\n" "$(timestamp)" "$RED" "$NC" "$1" | tee -a "$LOG_FILE"; HAS_ERROR=1; ERROR_TEXT+=" - $1"$'\n'; } + +echo "------------------------------------------------------------" +printf "%b\n" "$BOLD CompAI Device Setup (macOS)$NC" +echo "Organization: $ORG_ID" +echo "Employee: $EMPLOYEE_ID" +echo "Date: $(timestamp)" +echo "Log file: $LOG_FILE" +echo "------------------------------------------------------------" +echo + +# Determine if we need sudo for the fleet directory +SUDO="" +if [ ! -d "$FLEET_DIR" ]; then + log_info "Creating directory: $FLEET_DIR" + if mkdir -p "$FLEET_DIR" 2>>"$LOG_FILE"; then + : + else + log_warn "No write access creating $FLEET_DIR; retrying with sudo (you may be prompted for your password)." + if sudo mkdir -p "$FLEET_DIR" 2>>"$LOG_FILE"; then + SUDO="sudo" + else + log_error "Failed to create directory $FLEET_DIR even with sudo." + fi + fi +fi + +if [ -d "$FLEET_DIR" ] && [ ! -w "$FLEET_DIR" ]; then + SUDO="sudo" +fi + +if [ -z "$SUDO" ]; then + log_info "Using directory: $FLEET_DIR (no sudo needed)" +else + log_info "Using directory: $FLEET_DIR (sudo required)" +fi + +# Write marker files +if [ -d "$FLEET_DIR" ]; then + log_info "Writing organization marker file..." + if printf "%s" "$ORG_ID" | $SUDO tee "$FLEET_DIR/$ORG_ID" >/dev/null 2>>"$LOG_FILE"; then + log_info "[OK] Organization marker: $FLEET_DIR/$ORG_ID" + else + log_error "Failed writing organization marker to $FLEET_DIR/$ORG_ID" + fi + + log_info "Writing employee marker file..." + if printf "%s" "$EMPLOYEE_ID" | $SUDO tee "$FLEET_DIR/$EMPLOYEE_ID" >/dev/null 2>>"$LOG_FILE"; then + log_info "[OK] Employee marker: $FLEET_DIR/$EMPLOYEE_ID" + else + log_error "Failed writing employee marker to $FLEET_DIR/$EMPLOYEE_ID" + fi + + # Permissions + $SUDO chmod 755 "$FLEET_DIR" 2>>"$LOG_FILE" || log_warn "Could not chmod 755 on $FLEET_DIR" + $SUDO chmod 644 "$FLEET_DIR/$ORG_ID" 2>>"$LOG_FILE" || log_warn "Could not chmod 644 on $FLEET_DIR/$ORG_ID" + $SUDO chmod 644 "$FLEET_DIR/$EMPLOYEE_ID" 2>>"$LOG_FILE" || log_warn "Could not chmod 644 on $FLEET_DIR/$EMPLOYEE_ID" +else + log_error "Directory not available: $FLEET_DIR" +fi + +# Verify markers +echo +log_info "Verifying markers..." +if [ -f "$FLEET_DIR/$EMPLOYEE_ID" ]; then + log_info "[OK] Employee marker file present." +else + log_error "Employee marker file missing at $FLEET_DIR/$EMPLOYEE_ID" +fi + +# Summary +echo +echo "------------------------------------------------------------" +if [ "$HAS_ERROR" -eq 0 ]; then + printf "%b\n" "$GREEN RESULT: SUCCESS $NC" + echo "Setup completed successfully for $EMPLOYEE_ID." + echo "Files created in: $FLEET_DIR" +else + printf "%b\n" "$RED RESULT: COMPLETED WITH ISSUES $NC" + echo "One or more steps did not complete successfully. Details:" + printf "%b" "$ERROR_TEXT" + echo + echo "Next steps:" + echo " - Take a screenshot of this window." + echo " - Attach the log file from: $LOG_FILE" + echo " - Share both with your CompAI support contact." +fi +echo "------------------------------------------------------------" +echo +read -r -p "Press Return to close this window..." _ +exit $HAS_ERROR`; +} diff --git a/apps/portal/src/app/api/download-agent/scripts/windows.ts b/apps/portal/src/app/api/download-agent/scripts/windows.ts new file mode 100644 index 000000000..62183d425 --- /dev/null +++ b/apps/portal/src/app/api/download-agent/scripts/windows.ts @@ -0,0 +1,222 @@ +import type { ScriptConfig } from '../types'; + +export function generateWindowsScript(config: ScriptConfig): string { + const { orgId, employeeId, fleetDevicePath } = config; + + const script = `@echo off +title CompAI Device Setup +setlocal EnableExtensions EnableDelayedExpansion +color 0A + +REM ========================= +REM Variables +REM ========================= +set "ORG_ID=${orgId}" +set "EMPLOYEE_ID=${employeeId}" +set "PRIMARY_DIR=${fleetDevicePath}" +set "FALLBACK_DIR=C:\\Users\\Public\\CompAI\\Fleet" +set "CHOSEN_DIR=" +set "LOG_FILE=" +set "HAS_ERROR=0" +set "ERRORS=" +set "EXIT_CODE=0" +REM newline token (exactly this 2-line shape) +set "nl=^ +" + +REM --- bootstrap log (updated once CHOSEN_DIR is known) --- +set "LOG_FILE=%~dp0setup.log" + +goto :main + +REM ======================================================= +REM Subroutines (placed AFTER main to avoid early execution) +REM ======================================================= +:log_msg +setlocal EnableDelayedExpansion +set "msg=%~1" +echo [%date% %time%] !msg! +>>"%LOG_FILE%" echo [%date% %time%] !msg! +endlocal & exit /b 0 + +:log_run +setlocal EnableDelayedExpansion +set "cmdline=%*" +echo [%date% %time%] CMD: !cmdline! +>>"%LOG_FILE%" echo [%date% %time%] CMD: !cmdline! +%* +set "rc=!errorlevel!" +if not "!rc!"=="0" ( + echo [%date% %time%] ERR !rc!: !cmdline! + >>"%LOG_FILE%" echo [%date% %time%] ERR !rc!: !cmdline! +) +endlocal & set "LAST_RC=%rc%" +exit /b %LAST_RC% + +REM ========================= +REM Main +REM ========================= +:main +call :log_msg "Script starting" + +REM Admin check +whoami /groups | find "S-1-16-12288" >nul 2>&1 +if errorlevel 1 ( + color 0E + echo This script must be run as Administrator. + echo Please right-click the file and select "Run as administrator". + echo. + echo Press any key to exit, then try again with Administrator privileges. + pause + exit /b 5 +) + +REM Relaunch persistent window +if not "%PERSIST%"=="1" ( + set "PERSIST=1" + call :log_msg "Re-launching in a persistent window" + start "CompAI Device Setup" cmd /k "%~f0 %*" + exit /b +) + +call :log_msg "Running with administrator privileges" +call :log_msg "Current directory: %cd%" +call :log_msg "Script path: %~f0" +call :log_msg "Switching working directory to script folder" +cd /d "%~dp0" +call :log_msg "New current directory: %cd%" +echo. + +REM Choose writable directory +call :log_msg "Choosing destination directory; primary=%PRIMARY_DIR% fallback=%FALLBACK_DIR%" +if exist "%PRIMARY_DIR%\\*" set "CHOSEN_DIR=%PRIMARY_DIR%" +if not defined CHOSEN_DIR call :log_run mkdir "%PRIMARY_DIR%" +if not defined CHOSEN_DIR if exist "%PRIMARY_DIR%\\*" set "CHOSEN_DIR=%PRIMARY_DIR%" + +if not defined CHOSEN_DIR call :log_msg "Primary not available; trying fallback" +if not defined CHOSEN_DIR if exist "%FALLBACK_DIR%\\*" set "CHOSEN_DIR=%FALLBACK_DIR%" +if not defined CHOSEN_DIR call :log_run mkdir "%FALLBACK_DIR%" +if not defined CHOSEN_DIR if exist "%FALLBACK_DIR%\\*" set "CHOSEN_DIR=%FALLBACK_DIR%" + +if not defined CHOSEN_DIR ( + color 0E + call :log_msg "WARNING: No writable directory found" + echo Primary attempted: "%PRIMARY_DIR%" + echo Fallback attempted: "%FALLBACK_DIR%" + echo [%date% %time%] No writable directory found. Primary: %PRIMARY_DIR%, Fallback: %FALLBACK_DIR% >> "%~dp0setup.log" + set "LOG_FILE=%~dp0setup.log" + set "HAS_ERROR=1" + set "ERRORS=!ERRORS!- No writable directory found (Primary: %PRIMARY_DIR%, Fallback: %FALLBACK_DIR%).!nl!" + set "EXIT_CODE=1" +) else ( + set "MARKER_DIR=%CHOSEN_DIR%" + if not "!MARKER_DIR:~-1!"=="\\" set "MARKER_DIR=!MARKER_DIR!\\" + + REM switch the log file to the chosen directory, carry over bootstrap logs + set "FINAL_LOG=!MARKER_DIR!setup.log" + if /i not "%LOG_FILE%"=="%FINAL_LOG%" ( + call :log_msg "Switching log to !FINAL_LOG!" + if exist "%LOG_FILE%" type "%LOG_FILE%" >> "!FINAL_LOG!" & del "%LOG_FILE%" + set "LOG_FILE=!FINAL_LOG!" + ) + call :log_msg "Using directory: !MARKER_DIR!" +) +echo Logs will be written to: !LOG_FILE! +echo. + +REM Write marker files +if defined CHOSEN_DIR ( + call :log_msg "Writing organization marker file" + call :log_msg "Preparing to write org marker to !MARKER_DIR!!ORG_ID!" + call :log_run cmd /c "(echo %ORG_ID%) > \"!MARKER_DIR!!ORG_ID!\"" + if errorlevel 1 ( + color 0E + call :log_msg "WARNING: Failed writing organization marker file to !MARKER_DIR!" + echo [%date% %time%] Failed writing org marker file >> "%LOG_FILE%" + set "HAS_ERROR=1" + set "ERRORS=!ERRORS!- Failed writing organization marker file.!nl!" + set "EXIT_CODE=1" + ) else ( + call :log_msg "[OK] Organization marker file: !MARKER_DIR!!ORG_ID!" + ) + + call :log_msg "Writing employee marker file" + call :log_msg "Preparing to write employee marker to !MARKER_DIR!!EMPLOYEE_ID!" + call :log_run cmd /c "(echo %EMPLOYEE_ID%) > \"!MARKER_DIR!!EMPLOYEE_ID!\"" + if errorlevel 1 ( + color 0E + call :log_msg "WARNING: Failed writing employee marker file to !MARKER_DIR!" + echo [%date% %time%] Failed writing employee marker file >> "%LOG_FILE%" + set "HAS_ERROR=1" + set "ERRORS=!ERRORS!- Failed writing employee marker file.!nl!" + set "EXIT_CODE=1" + ) else ( + call :log_msg "[OK] Employee marker file: !MARKER_DIR!!EMPLOYEE_ID!" + ) +) + +REM Permissions +if defined CHOSEN_DIR ( + call :log_msg "Setting permissions on marker directory" + call :log_run icacls "!MARKER_DIR!" /inheritance:e + + call :log_msg "Granting read to SYSTEM and Administrators on org marker" + call :log_run icacls "!MARKER_DIR!!ORG_ID!" /grant *S-1-5-18:R *S-1-5-32-544:R + + call :log_msg "Granting read to SYSTEM and Administrators on employee marker" + call :log_run icacls "!MARKER_DIR!!EMPLOYEE_ID!" /grant *S-1-5-18:R *S-1-5-32-544:R +) + +REM Verify +echo. +echo Verifying markers... +if defined CHOSEN_DIR ( + call :log_msg "Verifying marker exists: !MARKER_DIR!!EMPLOYEE_ID!" + if not exist "!MARKER_DIR!!EMPLOYEE_ID!" ( + color 0E + call :log_msg "WARNING: Employee marker file missing at !MARKER_DIR!!EMPLOYEE_ID!" + echo [%date% %time%] Verification failed: employee marker file missing >> "!LOG_FILE!" + set "HAS_ERROR=1" + set "ERRORS=!ERRORS!- Employee marker file missing at !MARKER_DIR!!EMPLOYEE_ID!!.!nl!" + set "EXIT_CODE=2" + ) else ( + call :log_msg "[OK] Employee marker file present: !MARKER_DIR!!EMPLOYEE_ID!" + ) +) +rem Skipping registry checks per request + +REM Result / Exit +echo. +echo ------------------------------------------------------------ +if "%HAS_ERROR%"=="0" ( + color 0A + echo RESULT: SUCCESS + echo Setup completed successfully for %EMPLOYEE_ID%. + if defined CHOSEN_DIR echo Files created in: !CHOSEN_DIR! + echo Log file: !LOG_FILE! + call :log_msg "RESULT: SUCCESS" +) else ( + color 0C + echo RESULT: COMPLETED WITH ISSUES + echo One or more steps did not complete successfully. Details: + echo. + echo !ERRORS! + echo. + echo Next steps: + echo - Take a screenshot of this window. + echo - Attach the log file from: !LOG_FILE! + echo - Share both with your CompAI support contact. + call :log_msg "RESULT: COMPLETED WITH ISSUES (exit=%EXIT_CODE%)" +) +echo ------------------------------------------------------------ +echo. +echo Press any key to close this window. This will not affect installation. +pause +if "%HAS_ERROR%"=="0" (exit /b 0) else (exit /b %EXIT_CODE%) + +REM End of main +goto :eof +`; + + return script.replace(/\n/g, '\r\n'); +} diff --git a/apps/portal/src/app/api/download-agent/token/route.ts b/apps/portal/src/app/api/download-agent/token/route.ts index fabaf8032..883f3e8d1 100644 --- a/apps/portal/src/app/api/download-agent/token/route.ts +++ b/apps/portal/src/app/api/download-agent/token/route.ts @@ -49,19 +49,9 @@ export async function POST(req: NextRequest) { userId: session.user.id, }); - // Check environment configuration for fleet label creation - const fleetDevicePathMac = process.env.FLEET_DEVICE_PATH_MAC; - const fleetDevicePathWindows = process.env.FLEET_DEVICE_PATH_WINDOWS; - - if (!fleetDevicePathMac || !fleetDevicePathWindows) { - logger('Fleet device paths not configured in token route', { - fleetDevicePathMac: !!fleetDevicePathMac, - fleetDevicePathWindows: !!fleetDevicePathWindows, - }); - return new NextResponse('Server configuration error: Fleet device paths are missing.', { - status: 500, - }); - } + // Hardcoded device marker paths used by the setup scripts + const fleetDevicePathMac = '/Users/Shared/.fleet'; + const fleetDevicePathWindows = 'C:\\ProgramData\\CompAI\\Fleet'; // Create Fleet label try {