Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Local Netlify folder
.netlify
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# github-viewer
# github-viewer

## github app setup

Follow [this guide](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) on how to create a GitHub App and add the homepage URL and callback URL according to your netlify app.

**Homepage URL**: https://your-netlify-prefix.netlify.app/

**Callback URL**: https://your-netlify-prefix.app/callback


## netlify setup

For this you can follow [this getting started page](https://docs.netlify.com/cli/get-started).

But these three commands should be enough:

```bash
npm install netlify-cli -g
netlify login
netlify init
```

After this add these env vars to netlify:

**Any env var without the prefix VITE will not be available on the client side**

```
GITHUB_CLIENT_SECRET=<githubAppSecret>
VITE_GITHUB_CLIENT_ID=<githubAppId>
VITE_CODERS_INITIAL=<githubCoders> // username1,displayName1|username2,displayName2|...
```
Binary file modified bun.lockb
Binary file not shown.
9 changes: 9 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[build]
command = "bun run build"
functions = "netlify/functions"
publish = "dist"

[[redirects]]
from = "/*"
to = "/index.html"
status = 200
51 changes: 51 additions & 0 deletions netlify/functions/exchange-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fetch from 'node-fetch';
import { Handler, HandlerEvent } from '@netlify/functions';

const handler: Handler = async (event: HandlerEvent) => {
const client_id = process.env.VITE_GITHUB_CLIENT_ID!;
const client_secret = process.env.GITHUB_CLIENT_SECRET!;
const code = event.body ? JSON.parse(event.body).code : null;

if (!code) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Code is required' })
};
}

try {
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
client_id,
client_secret,
code
})
});

const data: any = await response.json();

if (data.error) {
return {
statusCode: 400,
body: JSON.stringify({ error: data.error })
};
}

return {
statusCode: 200,
body: JSON.stringify(data)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
};
}
};

export { handler };
51 changes: 51 additions & 0 deletions netlify/functions/refresh-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fetch from 'node-fetch';
import { Handler, HandlerEvent } from '@netlify/functions';

const handler: Handler = async (event: HandlerEvent) => {
const client_id = process.env.VITE_GITHUB_CLIENT_ID!;
const client_secret = process.env.GITHUB_CLIENT_SECRET!;
const refresh_token = event.body ? JSON.parse(event.body).refreshToken : null;

if (!refresh_token) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'refresh_token is required' })
};
}

try {
const response = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
client_id,
client_secret,
refresh_token
})
});

const data: any = await response.json();

if (data.error) {
return {
statusCode: 400,
body: JSON.stringify({ error: data.error })
};
}

return {
statusCode: 200,
body: JSON.stringify(data)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' })
};
}
};

export { handler };
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
"@mui/icons-material": "5.14.12",
"@mui/joy": "5.0.0-beta.9",
"@mui/material": "5.14.12",
"@netlify/functions": "2.7.0",
"axios": "1.5.1",
"graphql": "16.8.1",
"graphql-request": "7.0.1",
"immer": "10.0.3",
"node-fetch": "3.3.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "6.23.1",
Expand Down
32 changes: 30 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
import { CssVarsProvider } from "@mui/joy/styles";
import { Box, CssBaseline } from "@mui/joy";
import { Box, Button, CssBaseline } from "@mui/joy";
import { PullRequestView } from "./pr";
import { OpenInNew } from "@mui/icons-material";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { AuthCallback } from "./AuthCallback";

const router = createBrowserRouter([
{
path: "/",
element: <PullRequestView />,
},
{
path: "/login",
element: (
<Button
component="a"
href={`https://github.com/login/oauth/authorize?client_id=${
import.meta.env.VITE_GITHUB_CLIENT_ID
}`}
startDecorator={<OpenInNew />}
>
Login with Github
</Button>
),
},
{
path: "/callback",
element: <AuthCallback />,
},
]);

export function App() {
return (
Expand All @@ -21,7 +49,7 @@ export function App() {
overflowY: "hidden",
}}
>
<PullRequestView />
<RouterProvider router={router} />
</Box>
</Box>
</CssVarsProvider>
Expand Down
52 changes: 52 additions & 0 deletions src/AuthCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Box, Typography } from "@mui/joy";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { storeToken } from "./utils";

export const AuthCallback = () => {
const navigate = useNavigate();
const [urlSearchParams] = useSearchParams();
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<string[]>([]);

useEffect(() => {
if (urlSearchParams.get("code") != null) {
setLoading(true);
fetch("/.netlify/functions/exchange-token", {
method: "POST",
body: JSON.stringify({ code: urlSearchParams.get("code") }),
})
.then((response) => response.json())
.then((data) => {
setLoading(false);
storeToken(data)
navigate("/");
})
.catch((error) => {
setLoading(false);
setErrors([error.message])
});
} else {
setErrors(["No code found in URL."])
}

}, [urlSearchParams]);

return (
<Box sx={{ display: "flex", minHeight: "100dvh", overflowY: "hidden" }}>
<Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
height: "100dvh",
justifyContent: "center",
alignItems: "center",
}}
>
{loading && errors.length === 0 ? "Loading..." : null}
{!loading && errors.length !== 0 ? errors.map(error => <Typography>{error}</Typography>) : null}
</Box>
</Box>
);
};
10 changes: 8 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { GithubGraphqlClient } from "./GithubGraphqlClient.ts";
import { getAccessToken } from "../utils";

const AUTH_TOKEN = import.meta.env.VITE_AUTH_TOKEN;
let api: GithubGraphqlClient;

export const api = new GithubGraphqlClient(AUTH_TOKEN);
export async function getApi() {
if (!api) {
api = new GithubGraphqlClient(await getAccessToken());
}
return api;
}
3 changes: 2 additions & 1 deletion src/api/fetchAllOpenPrs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getApi } from "./api.ts";
import { mapResult } from "./mapResult.ts";
import { PullRequest } from "./types.ts";
import { api } from "./api.ts";

export async function fetchAllOpenPrs(
usernames: string[],
Expand All @@ -11,6 +11,7 @@ export async function fetchAllOpenPrs(
const openPRs: PullRequest[] = [];

while (hasNextPage) {
const api = await getApi();
const response = await api.getPullRequests(usernames, "open", 50, after);
const mappedResult = mapResult(response, baseUsername);

Expand Down
47 changes: 47 additions & 0 deletions src/utils/authHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
interface TokenResponse {
access_token: string;
refresh_token: string;
expires_in: number;
refresh_token_expires_in: number;
}

export function storeToken({
access_token,
refresh_token,
expires_in,
refresh_token_expires_in
}: TokenResponse) {
localStorage.setItem('github_access_token', access_token);
localStorage.setItem('github_refresh_token', refresh_token);
localStorage.setItem('github_token_expiry', (Date.now() + expires_in * 1000).toString());
localStorage.setItem('github_refresh_token_expiry', (Date.now() + refresh_token_expires_in * 1000).toString());
}

async function refreshAccessToken() {
const refreshToken = localStorage.getItem('github_refresh_token');
if (!refreshToken) {
throw new Error('No refresh token found');
}
fetch("/.netlify/functions/refresh-token", {
method: "POST",
body: JSON.stringify({ refreshToken}),
})
.then((response) => response.json())
.then((data) => {
storeToken(data)
})
}

export async function getAccessToken() {
const token = localStorage.getItem('github_access_token');
const expiry = localStorage.getItem('github_token_expiry');
if (!token || !expiry) {
throw new Error('No token found');
}

if (parseInt(expiry) < Date.now()) {
await refreshAccessToken();
}

return token ?? '';
}
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./timeAgo.ts";
export * from "./authHelper.ts";