Skip to content
189 changes: 189 additions & 0 deletions backend/src/controllers/setup.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,61 @@ import app from '../index.js';
import StatusService from '../services/status.service.js';
import logger from '../services/logger.js';

// Type definitions for the diagnostic response
interface OctokitTestResult {
success: boolean;
appName?: string;
appOwner?: string;
permissions?: Record<string, string | undefined>;
error?: string;
}

interface InstallationDiagnostic {
index: number;
installationId: number;
accountLogin: string;
accountId: string | number;
accountType: string;
accountAvatarUrl: string;
appId: number;
appSlug: string;
targetType: string;
permissions: Record<string, string | undefined>;
events: string[];
createdAt: string;
updatedAt: string;
suspendedAt: string | null;
suspendedBy: { login: string; id: number } | null;
hasOctokit: boolean;
octokitTest: OctokitTestResult | null;
isValid: boolean;
validationErrors: string[];
}

interface AppInfo {
name: string;
description: string;
owner: string;
htmlUrl: string;
permissions: Record<string, string | undefined>;
events: string[];
}

interface DiagnosticsResponse {
timestamp: string;
appConnected: boolean;
totalInstallations: number;
installations: InstallationDiagnostic[];
errors: string[];
appInfo: AppInfo | null;
summary: {
validInstallations: number;
invalidInstallations: number;
organizationNames: string[];
accountTypes: Record<string, number>;
};
}

class SetupController {
async registrationComplete(req: Request, res: Response) {
try {
Expand Down Expand Up @@ -112,6 +167,140 @@ class SetupController {
}
}

async validateInstallations(req: Request, res: Response) {
try {
const diagnostics: DiagnosticsResponse = {
timestamp: new Date().toISOString(),
appConnected: !!app.github.app,
totalInstallations: app.github.installations.length,
installations: [],
errors: [],
appInfo: null,
summary: {
validInstallations: 0,
invalidInstallations: 0,
organizationNames: [],
accountTypes: {}
}
};

// Basic app validation
if (!app.github.app) {
diagnostics.errors.push('GitHub App is not initialized');
return res.json(diagnostics);
}

// Validate each installation
for (let i = 0; i < app.github.installations.length; i++) {
const { installation, octokit } = app.github.installations[i];

const installationDiag: InstallationDiagnostic = {
index: i,
installationId: installation.id,
accountLogin: installation.account?.login || 'MISSING',
accountId: installation.account?.id || 'MISSING',
accountType: installation.account?.type || 'MISSING',
accountAvatarUrl: installation.account?.avatar_url || 'MISSING',
appId: installation.app_id,
appSlug: installation.app_slug,
targetType: installation.target_type,
permissions: installation.permissions || {},
events: installation.events || [],
createdAt: installation.created_at,
updatedAt: installation.updated_at,
suspendedAt: installation.suspended_at,
suspendedBy: installation.suspended_by,
hasOctokit: !!octokit,
octokitTest: null,
isValid: true,
validationErrors: []
};

// Validate required fields
if (!installation.account?.login) {
installationDiag.isValid = false;
installationDiag.validationErrors.push('Missing account.login (organization name)');
}

if (!installation.account?.id) {
installationDiag.isValid = false;
installationDiag.validationErrors.push('Missing account.id');
}

if (!installation.account?.type) {
installationDiag.isValid = false;
installationDiag.validationErrors.push('Missing account.type');
}

// Test Octokit functionality
if (octokit) {
try {
// Test basic API call with the installation's octokit
const authTest = await octokit.rest.apps.getAuthenticated();
installationDiag.octokitTest = {
success: true,
appName: authTest.data?.name || 'Unknown',
appOwner: (authTest.data?.owner && 'login' in authTest.data.owner) ? authTest.data.owner.login : 'Unknown',
permissions: authTest.data?.permissions || {}
};
} catch (error) {
installationDiag.octokitTest = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
installationDiag.isValid = false;
installationDiag.validationErrors.push(`Octokit API test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
} else {
installationDiag.isValid = false;
installationDiag.validationErrors.push('Octokit instance is missing');
}

// Update summary
if (installationDiag.isValid) {
diagnostics.summary.validInstallations++;
if (installation.account?.login) {
diagnostics.summary.organizationNames.push(installation.account.login);
}
} else {
diagnostics.summary.invalidInstallations++;
}

// Track account types
const accountType = installation.account?.type || 'Unknown';
diagnostics.summary.accountTypes[accountType] = (diagnostics.summary.accountTypes[accountType] || 0) + 1;

diagnostics.installations.push(installationDiag);
}

// Additional app-level diagnostics
try {
const appInfo = await app.github.app.octokit.rest.apps.getAuthenticated();
diagnostics.appInfo = {
name: appInfo.data?.name || 'Unknown',
description: appInfo.data?.description || 'No description',
owner: (appInfo.data?.owner && 'login' in appInfo.data.owner) ? appInfo.data.owner.login : 'Unknown',
htmlUrl: appInfo.data?.html_url || 'Unknown',
permissions: appInfo.data?.permissions || {},
events: appInfo.data?.events || []
};
} catch (error) {
diagnostics.errors.push(`Failed to get app info: ${error instanceof Error ? error.message : 'Unknown error'}`);
}

// Sort organization names for easier reading
diagnostics.summary.organizationNames.sort();

res.json(diagnostics);
} catch (error) {
logger.error('Installation validation failed', error);
res.status(500).json({
error: 'Installation validation failed',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}


}

Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ router.get('/setup/manifest', setupController.getManifest);
router.post('/setup/existing-app', setupController.addExistingApp);
router.post('/setup/db', setupController.setupDB);
router.get('/setup/status', setupController.setupStatus);
router.get('/setup/validate-installations', setupController.validateInstallations);

router.get('/status', setupController.getStatus);

Expand Down
8 changes: 3 additions & 5 deletions backend/src/services/status.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface StatusType {
};
installations: {
installation: Endpoints["GET /app/installations"]["response"]["data"][0]
repos: Endpoints["GET /installation/repositories"]["response"]["data"]["repositories"];
repos: Endpoints["GET /app/installations"]["response"]["data"];
}[];
surveyCount: number;
auth?: {
Expand Down Expand Up @@ -56,12 +56,10 @@ class StatusService {

status.installations = [];
for (const installation of app.github.installations) {
const repos = await installation.octokit.paginate<Endpoints["GET /installation/repositories"]["response"]["data"]["repositories"][0]>(
installation.installation.repositories_url
);
const repos = await installation.octokit.request(installation.installation.repositories_url);
status.installations.push({
installation: installation.installation,
repos: repos
repos: repos.data.repositories
});
}

Expand Down
Loading
Loading