Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"graphql-subscriptions": "^2.0.0",
"graphql-upload-minimal": "^1.6.1",
"graphql-ws": "^5.16.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"markdown-to-txt": "^2.0.1",
"nodemailer": "^6.10.0",
Expand Down
17 changes: 12 additions & 5 deletions backend/src/github/github.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { UserService } from 'src/user/user.service';
export class GitHuController {
private readonly webhookMiddleware;

constructor(private readonly gitHubAppService: GitHubAppService, private readonly userService: UserService) {
constructor(
private readonly gitHubAppService: GitHubAppService,
private readonly userService: UserService,
) {
// Get the App instance from the service
const app = this.gitHubAppService.getApp();

Expand All @@ -24,7 +27,7 @@ export class GitHuController {
@Post('webhook')
async handleWebhook(@Req() req: Request, @Res() res: Response) {
console.log('📩 Received POST /github/webhook');

return this.webhookMiddleware(req, res, (error?: any) => {
if (error) {
console.error('Webhook middleware error:', error);
Expand All @@ -35,13 +38,17 @@ export class GitHuController {
}
});
}

@Post('storeInstallation')
async storeInstallation(
@Body() body: { installationId: string, githubCode: string },
@Body() body: { installationId: string; githubCode: string },
@GetUserIdFromToken() userId: string,
) {
await this.userService.bindUserIdAndInstallId(userId, body.installationId, body.githubCode);
await this.userService.bindUserIdAndInstallId(
userId,
body.installationId,
body.githubCode,
);
return { success: true };
}
}
9 changes: 8 additions & 1 deletion backend/src/github/github.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ import { UserModule } from 'src/user/user.module';
forwardRef(() => UserModule),
],
controllers: [GitHuController],
providers: [ProjectService, ProjectGuard, GitHubAppService, GitHubService, ConfigService, ChatService],
providers: [
ProjectService,
ProjectGuard,
GitHubAppService,
GitHubService,
ConfigService,
ChatService,
],
exports: [GitHubService],
})
export class GitHubModule {}
224 changes: 126 additions & 98 deletions backend/src/github/github.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,26 @@ import { Repository } from 'typeorm';
@Injectable()
export class GitHubService {
private readonly logger = new Logger(GitHubService.name);

private readonly appId: string;
private privateKey: string;
private ignored = ['node_modules', '.git', '.gitignore', '.env'];

constructor(
private configService: ConfigService,
private configService: ConfigService,
@InjectRepository(Project)
private projectsRepository: Repository<Project>,)
{

private projectsRepository: Repository<Project>,
) {
this.appId = this.configService.get<string>('GITHUB_APP_ID');

const privateKeyPath = this.configService.get<string>('GITHUB_PRIVATE_KEY_PATH');

const privateKeyPath = this.configService.get<string>(
'GITHUB_PRIVATE_KEY_PATH',
);

if (!privateKeyPath) {
throw new Error('GITHUB_PRIVATE_KEY_PATH is not set in environment variables');
throw new Error(
'GITHUB_PRIVATE_KEY_PATH is not set in environment variables',
);
}

this.logger.log(`Reading GitHub private key from: ${privateKeyPath}`);
Expand All @@ -50,9 +53,9 @@ export class GitHubService {
// 1) Create a JWT (valid for ~10 minutes)
const now = Math.floor(Date.now() / 1000);
const payload = {
iat: now, // Issued at time
exp: now + 600, // JWT expiration (10 minute maximum)
iss: this.appId, // Your GitHub App's App ID
iat: now, // Issued at time
exp: now + 600, // JWT expiration (10 minute maximum)
iss: this.appId, // Your GitHub App's App ID
};

const gitHubAppJwt = jwt.sign(payload, this.privateKey, {
Expand All @@ -77,13 +80,16 @@ export class GitHubService {
return token;
}


async exchangeOAuthCodeForToken(code: string): Promise<string> {
const clientId = this.configService.get<string>('GITHUB_CLIENT_ID');
const clientSecret = this.configService.get<string>('GITHUB_CLIENT_SECRET');

console.log('Exchanging OAuth Code:', { code, clientId, clientSecretExists: !!clientSecret });


console.log('Exchanging OAuth Code:', {
code,
clientId,
clientSecretExists: !!clientSecret,
});

try {
const response = await axios.post(
'https://github.com/login/oauth/access_token',
Expand All @@ -98,30 +104,37 @@ export class GitHubService {
},
},
);

console.log('GitHub Token Exchange Response:', response.data);

if (response.data.error) {
console.error('GitHub OAuth error:', response.data);
throw new BadRequestException(`GitHub OAuth error: ${response.data.error_description}`);
throw new BadRequestException(
`GitHub OAuth error: ${response.data.error_description}`,
);
}

const accessToken = response.data.access_token;
if (!accessToken) {
throw new Error('GitHub token exchange failed: No access token returned.');
throw new Error(
'GitHub token exchange failed: No access token returned.',
);
}

return accessToken;
} catch (error: any) {
console.error('OAuth exchange failed:', error.response?.data || error.message);
console.error(
'OAuth exchange failed:',
error.response?.data || error.message,
);
// throw new Error(`GitHub OAuth exchange failed: ${error.response?.data?.error_description || error.message}`);
}
}

/**
* Create a new repository under the *user's* account.
* If you need an org-level repo, use POST /orgs/{org}/repos.
*/
*/
async createUserRepo(
repoName: string,
isPublic: boolean,
Expand Down Expand Up @@ -151,12 +164,17 @@ export class GitHubService {
const data = response.data;
return {
owner: data.owner.login, // e.g. "octocat"
repo: data.name, // e.g. "my-new-repo"
htmlUrl: data.html_url, // e.g. "https://github.com/octocat/my-new-repo"
repo: data.name, // e.g. "my-new-repo"
htmlUrl: data.html_url, // e.g. "https://github.com/octocat/my-new-repo"
};
}

async pushMultipleFiles(installationToken: string, owner: string, repo: string, files: string[]) {
async pushMultipleFiles(
installationToken: string,
owner: string,
repo: string,
files: string[],
) {
for (const file of files) {
const fileName = path.basename(file);
await this.pushFileContent(
Expand All @@ -165,88 +183,98 @@ export class GitHubService {
repo,
file,
`myFolder/${fileName}`,
'Initial commit of file ' + fileName
'Initial commit of file ' + fileName,
);
}
}

/**
* Push a single file to the given path in the repo using GitHub Contents API.
*
*
* @param relativePathInRepo e.g. "backend/index.js" or "frontend/package.json"
*/
async pushFileContent(
installationToken: string,
owner: string,
repo: string,
localFilePath: string,
relativePathInRepo: string,
commitMessage: string,
) {
const fileBuffer = fs.readFileSync(localFilePath);
const base64Content = fileBuffer.toString('base64');

const url = `https://api.github.com/repos/${owner}/${repo}/contents/${relativePathInRepo}`;

await axios.put(
url,
{
message: commitMessage,
content: base64Content,
},
{
headers: {
Authorization: `token ${installationToken}`,
Accept: 'application/vnd.github.v3+json',
},
async pushFileContent(
installationToken: string,
owner: string,
repo: string,
localFilePath: string,
relativePathInRepo: string,
commitMessage: string,
) {
const fileBuffer = fs.readFileSync(localFilePath);
const base64Content = fileBuffer.toString('base64');

const url = `https://api.github.com/repos/${owner}/${repo}/contents/${relativePathInRepo}`;

await axios.put(
url,
{
message: commitMessage,
content: base64Content,
},
{
headers: {
Authorization: `token ${installationToken}`,
Accept: 'application/vnd.github.v3+json',
},
);

this.logger.log(`Pushed file: ${relativePathInRepo} -> https://github.com/${owner}/${repo}`);
}

/**
* Recursively push all files in a local folder to the repo.
* Skips .git, node_modules, etc. (configurable)
*/
async pushFolderContent(
installationToken: string,
owner: string,
repo: string,
folderPath: string,
basePathInRepo: string, // e.g. "" or "backend"
) {
const entries = fs.readdirSync(folderPath, { withFileTypes: true });

for (const entry of entries) {

// Skip unwanted files
if (this.ignored.includes(entry.name)) {
continue;
}
},
);

this.logger.log(
`Pushed file: ${relativePathInRepo} -> https://github.com/${owner}/${repo}`,
);
}

const entryPath = path.join(folderPath, entry.name);
if (entry.isDirectory()) {
// Skip unwanted directories
if (entry.name === '.git' || entry.name === 'node_modules') {
continue;
}
// Recurse into subdirectory
const subDirInRepo = path.join(basePathInRepo, entry.name).replace(/\\/g, '/');
await this.pushFolderContent(installationToken, owner, repo, entryPath, subDirInRepo);
} else {
// It's a file; push it
const fileInRepo = path.join(basePathInRepo, entry.name).replace(/\\/g, '/');
await this.pushFileContent(
installationToken,
owner,
repo,
entryPath,
fileInRepo,
`Add file: ${fileInRepo}`,
);
/**
* Recursively push all files in a local folder to the repo.
* Skips .git, node_modules, etc. (configurable)
*/
async pushFolderContent(
installationToken: string,
owner: string,
repo: string,
folderPath: string,
basePathInRepo: string, // e.g. "" or "backend"
) {
const entries = fs.readdirSync(folderPath, { withFileTypes: true });

for (const entry of entries) {
// Skip unwanted files
if (this.ignored.includes(entry.name)) {
continue;
}

const entryPath = path.join(folderPath, entry.name);
if (entry.isDirectory()) {
// Skip unwanted directories
if (entry.name === '.git' || entry.name === 'node_modules') {
continue;
}
// Recurse into subdirectory
const subDirInRepo = path
.join(basePathInRepo, entry.name)
.replace(/\\/g, '/');
await this.pushFolderContent(
installationToken,
owner,
repo,
entryPath,
subDirInRepo,
);
} else {
// It's a file; push it
const fileInRepo = path
.join(basePathInRepo, entry.name)
.replace(/\\/g, '/');
await this.pushFileContent(
installationToken,
owner,
repo,
entryPath,
fileInRepo,
`Add file: ${fileInRepo}`,
);
}
}

}
}
Loading
Loading