Skip to content

Commit cbf0f34

Browse files
committed
added github connection with skills update
1 parent 2887f05 commit cbf0f34

5 files changed

Lines changed: 220 additions & 0 deletions

File tree

nextstep-backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import validateUser from "./middleware/validateUser";
1515
import loadOpenApiFile from "./openapi/openapi_loader";
1616
import resource_routes from './routes/resources_routes';
1717
import resume_routes from './routes/resume_routes';
18+
import githubRoutes from './routes/github_routes';
1819

1920
const specs = swaggerJsdoc(options);
2021

@@ -74,5 +75,6 @@ app.use('/user', usersRoutes);
7475
app.use('/resource', resource_routes);
7576
app.use('/room', roomsRoutes);
7677
app.use('/resume', resume_routes);
78+
app.use('/github', githubRoutes);
7779

7880
export { app, corsOptions };
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import axios from 'axios';
2+
import { Request, Response } from 'express';
3+
4+
const clientId = process.env.GITHUB_CLIENT_ID;
5+
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
6+
7+
export const handleGitHubOAuth = async (req: Request, res: Response) => {
8+
const { code } = req.body;
9+
10+
if (!code) {
11+
return res.status(400).json({ error: 'Authorization code is required' });
12+
}
13+
14+
try {
15+
const tokenResponse = await axios.post('https://github.com/login/oauth/access_token', {
16+
headers: { 'Content-Type': 'application/json' },
17+
client_id: clientId,
18+
client_secret: clientSecret,
19+
code: code,
20+
}) as any;
21+
22+
const tokenData = await tokenResponse.data;
23+
const params = new URLSearchParams(tokenData);
24+
const accessToken = params.get('access_token');
25+
26+
if (!accessToken) {
27+
return res.status(400).json({ error: 'Failed to retrieve access token' });
28+
}
29+
30+
const userResponse = await axios.get('https://api.github.com/user', {
31+
headers: { Authorization: `Bearer ${accessToken}` },
32+
}) as {status: number, data: { login?: string }, json: () => Promise<any>};
33+
34+
if (userResponse.status !== 200) {
35+
const errorText = await userResponse.data;
36+
return res.status(400).json({ error: errorText });
37+
}
38+
39+
const userData = await userResponse.data;
40+
res.json({ username: userData.login });
41+
} catch (error) {
42+
console.error('Error during GitHub OAuth:', error);
43+
res.status(500).json({ error: 'Internal server error' });
44+
}
45+
};
46+
47+
export const fetchGitHubRepos = async (req: Request, res: Response) => {
48+
const { username } = req.params;
49+
50+
try {
51+
const apiUrl = `https://api.github.com/users/${username}/repos`;
52+
const response = await axios.get(apiUrl) as {data: any, status: number};
53+
54+
if (response.status !== 200) {
55+
return res.status(400).json({ error: `Error fetching repos: ${response.status}` });
56+
}
57+
58+
const repos = await response.data;
59+
res.json(repos);
60+
} catch (error) {
61+
console.error('Error fetching repos:', error);
62+
res.status(500).json({ error: 'Internal server error' });
63+
}
64+
};
65+
66+
export const fetchRepoLanguages = async (req: Request, res: Response) => {
67+
const { repoUrl } = req.query;
68+
69+
if (!repoUrl) {
70+
return res.status(400).json({ error: 'Repository URL is required' });
71+
}
72+
73+
try {
74+
// Convert the repoUrl to the GitHub API URL if necessary
75+
const apiUrl = (repoUrl as string).replace('https://github.com/', 'https://api.github.com/repos/');
76+
const response = await axios.get(`${apiUrl}/languages`);
77+
78+
if (response.status !== 200) {
79+
return res.status(400).json({ error: `Error fetching languages: ${response.statusText}` });
80+
}
81+
82+
res.json(response.data);
83+
} catch (error) {
84+
console.error('Error fetching languages:', error);
85+
res.status(500).json({ error: 'Internal server error' });
86+
}
87+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import express from 'express';
2+
import { handleGitHubOAuth, fetchGitHubRepos, fetchRepoLanguages } from '../controllers/github_controller';
3+
4+
const router = express.Router();
5+
6+
router.post('/oauth', handleGitHubOAuth);
7+
router.get('/repos/:username', fetchGitHubRepos);
8+
router.get('/languages', fetchRepoLanguages);
9+
10+
export default router;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import api from "../serverApi.ts";
2+
3+
// Function to fetch GitHub repositories by username
4+
export const fetchGitHubRepos = async (username: string) => {
5+
try {
6+
const response = await api.get(`github/repos/${username}`) as any;
7+
if (response.status !== 200) {
8+
throw new Error(`Error fetching repos: ${response.statusText}`);
9+
}
10+
return response.data;
11+
} catch (error) {
12+
console.error(error);
13+
return [];
14+
}
15+
};
16+
17+
// Function to connect to GitHub by username
18+
export const connectToGitHub = async (username: string) => {
19+
if (!username) {
20+
throw new Error('Username is required to connect to GitHub.');
21+
}
22+
return await fetchGitHubRepos(username);
23+
};
24+
25+
// Function to initiate GitHub OAuth login (redirect only)
26+
export const initiateGitHubOAuth = () => {
27+
const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID; // Use environment variable
28+
const redirectUri = import.meta.env.VITE_GITHUB_REDIRECT_URI; // Use environment variable
29+
const scope = 'repo'; // Adjust scope as needed
30+
const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;
31+
window.location.href = authUrl; // Redirect the user to GitHub's authorization page
32+
};
33+
34+
// Function to handle GitHub OAuth token exchange and fetching user details
35+
export const handleGitHubOAuth = async (code: string) => {
36+
try {
37+
const response = await api.post('github/oauth', {
38+
headers: { 'Content-Type': 'application/json' },
39+
code: code,
40+
}) as {data: {username?: string, error?: string}, status: number};
41+
42+
if (response.status !== 200) {
43+
const errorText = await response.data.error;
44+
console.error('Backend OAuth Error:', errorText);
45+
throw new Error('Failed to authenticate with GitHub');
46+
}
47+
else if (!response.data.username) {
48+
console.error('Backend OAuth Error:', response.data.error);
49+
throw new Error('Failed to retrieve user information from GitHub');
50+
}
51+
52+
return response.data.username;
53+
} catch (error) {
54+
const err : Error = error as Error;
55+
console.error('Error during GitHub OAuth:', error);
56+
throw new Error('Internal server error: ' + err.message);
57+
}
58+
};
59+
60+
// Function to fetch languages for a specific repository
61+
export const fetchRepoLanguages = async (repoUrl: string) => {
62+
try {
63+
const response = await api.get(`github/languages?repoUrl=${encodeURIComponent(repoUrl)}`) as any;
64+
if (response.status !== 200) {
65+
throw new Error(`Error fetching languages: ${response.statusText}`);
66+
}
67+
return await response.data;
68+
} catch (error) {
69+
console.error(error);
70+
return {};
71+
}
72+
};

nextstep-frontend/src/pages/MainDashboard.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useEffect } from 'react';
22
import { Container, Box, Typography, Button, TextField, Chip, Stack, Grid, Paper, Autocomplete, MenuItem, Select, FormControl, InputLabel } from '@mui/material';
33
import { GitHub, LinkedIn } from '@mui/icons-material';
4+
import { connectToGitHub, initiateGitHubOAuth, fetchRepoLanguages, handleGitHubOAuth } from '../handlers/githubAuth';
45

56
const roles = [
67
'Software Engineer',
@@ -41,6 +42,7 @@ const MainDashboard: React.FC = () => {
4142
const [skills, setSkills] = useState<string[]>(() => JSON.parse(localStorage.getItem('skills') || '[]'));
4243
const [newSkill, setNewSkill] = useState('');
4344
const [selectedRole, setSelectedRole] = useState(() => localStorage.getItem('selectedRole') || '');
45+
const [repos, setRepos] = useState<{ id: number; name: string; html_url: string }[]>([]);
4446

4547
useEffect(() => {
4648
localStorage.setItem('aboutMe', aboutMe);
@@ -64,6 +66,40 @@ const MainDashboard: React.FC = () => {
6466
setSkills(skills.filter(skill => skill !== skillToDelete));
6567
};
6668

69+
const handleGitHubConnect = async () => {
70+
try {
71+
initiateGitHubOAuth();
72+
} catch (error) {
73+
console.error('Error initiating GitHub OAuth:', error);
74+
}
75+
};
76+
77+
useEffect(() => {
78+
const queryParams = new URLSearchParams(window.location.search);
79+
const code = queryParams.get('code');
80+
if (code) {
81+
const fetchGitHubData = async () => {
82+
try {
83+
const username = await handleGitHubOAuth(code);
84+
const fetchedRepos = await connectToGitHub(username);
85+
setRepos(fetchedRepos);
86+
87+
// Fetch languages and add them as skills
88+
const languagesSet = new Set(skills);
89+
for (const repo of fetchedRepos) {
90+
const repoLanguages = await fetchRepoLanguages(repo.html_url);
91+
Object.keys(repoLanguages).forEach((lang) => languagesSet.add(lang));
92+
}
93+
setSkills(Array.from(languagesSet));
94+
} catch (error) {
95+
console.error('Error fetching GitHub data:', error);
96+
}
97+
};
98+
99+
fetchGitHubData();
100+
}
101+
}, []);
102+
67103
return (
68104
<Container
69105
maxWidth="lg"
@@ -172,9 +208,22 @@ const MainDashboard: React.FC = () => {
172208
color="secondary"
173209
startIcon={<GitHub />}
174210
fullWidth
211+
onClick={handleGitHubConnect}
175212
>
176213
Connect to GitHub
177214
</Button>
215+
<Typography variant="h6" sx={{ mt: 3 }}>
216+
GitHub Repositories
217+
</Typography>
218+
<ul>
219+
{repos.map((repo) => (
220+
<li key={repo.id}>
221+
<a href={repo.html_url} target="_blank" rel="noopener noreferrer">
222+
{repo.name}
223+
</a>
224+
</li>
225+
))}
226+
</ul>
178227
</Paper>
179228
</Grid>
180229
</Grid>

0 commit comments

Comments
 (0)