Skip to content

Commit a66ccdb

Browse files
authored
Merge pull request #2 from karpikpl/feature/configuration
Feature/configuration
2 parents ec9f494 + e313383 commit a66ccdb

35 files changed

+2152
-140
lines changed

.dockerignore

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
dist
2-
node_modules
3-
.github
1+
**/.env
2+
**/.env.local
3+
.env*
4+
.dockerignore
5+
.git
6+
.gitignore
7+
**/node_modules/
8+
api/public
9+
dist/
10+
.history/
11+
.github/
12+
Dockerfile
13+
*.md

.env

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ VUE_APP_GITHUB_ORG=octodemo
1111
VUE_APP_GITHUB_ENT=
1212

1313
# Determines the GitHub Personal Access Token to use for API calls.
14-
# Create with scopes copilot, manage_billing:copilot, admin:enterprise, or manage_billing:enterprise, read:enterprise AND read:org
14+
# Create with scopes copilot, manage_billing:copilot or manage_billing:enterprise, read:enterprise AND read:org
1515
VUE_APP_GITHUB_TOKEN=
16+
17+
# GitHub Api Url
18+
# for proxy api - set to /api/github
19+
VUE_APP_GITHUB_API_URL=https://api.github.com

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ pnpm-debug.log*
2121
*.njsproj
2222
*.sln
2323
*.sw?
24+
.azure
25+
26+
api/public

Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ COPY . .
77
RUN npm run build
88

99
# Stage 2: Serve the application with Nginx
10-
FROM nginx:1.19 as production-stage
10+
FROM nginx:1.27 as production-stage
1111
COPY --from=build-stage /app/dist /usr/share/nginx/html
12+
COPY --from=build-stage /app/dist/assets/app-config.js /usr/share/nginx/html-template/app-config.template.js
13+
COPY ./docker-entrypoint.d/*.sh /docker-entrypoint.d/
14+
RUN chmod +x /docker-entrypoint.d/*.sh
1215
EXPOSE 80
13-
CMD ["nginx", "-g", "daemon off;"]
16+
CMD ["nginx", "-g", "daemon off;"]

README.md

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,18 @@ The language breakdown analysis tab also displays a table showing the Accepted P
7171
4. **Total Active Copilot Chat Users:** a bar chart that illustrates the total number of users who have actively interacted with Copilot over the past 28 days.
7272

7373
## Seat Analysis
74+
<p align="center">
75+
<img width="800" alt="image" src="https://github.com/github-copilot-resources/copilot-metrics-viewer/assets/54096296/51747194-df30-4bfb-8849-54a0510fffcb">
76+
</p>
77+
1. **Total Assigned:** This metric represents the total number of Copilot seats assigned within current organization/enterprise.
7478

75-
![image](https://github.com/DevOps-zhuang/copilot-metrics-viewer/assets/54096296/d1fa9d1d-4fab-4e87-84ba-7be189dd4dd0)
76-
77-
1. **Total Assigned:** This metric represents the total number of Copilot seats assigned within current organization.
78-
79-
2. **Assigned But Never Used:** This metric shows seats that were assigned but never within the current organization. The assigned timestamp is also displayed in the below chart.
79+
2. **Assigned But Never Used:** This metric shows seats that were assigned but never used within the current organization/enterprise. The assigned timestamp is also displayed in the chart.
8080

8181
3. **No Activity in the Last 7 days:** never used seats or seats used, but with no activity in the past 7 days.
8282

8383
4. **No Activity in the last 7 days (including never used seats):** a table to display seats that have had no activity in the past 7 days, ordered by the date of last activity. Seats that were used earlier are displayed at the top.
8484

8585

86-
8786
## Setup instructions
8887

8988
In the `.env` file, you can configure several environment variables that control the behavior of the application.
@@ -115,7 +114,7 @@ To access Copilot metrics from the last 28 days via the API and display actual d
115114
```
116115

117116
#### VUE_APP_GITHUB_TOKEN
118-
Specifies the GitHub Personal Access Token utilized for API requests. Generate this token with the following scopes: _copilot_, _manage_billing:copilot_, _manage_billing:enterprise_, _read:enterprise_, _admin:org_.
117+
Specifies the GitHub Personal Access Token utilized for API requests. Generate this token with the following scopes: _copilot_, _manage_billing:copilot_, _manage_billing:enterprise_, _read:enterprise_, _read:org_.
119118

120119
```
121120
VUE_APP_GITHUB_TOKEN=
@@ -138,10 +137,53 @@ docker build -t copilot-metrics-viewer .
138137

139138
### Docker run
140139
```
141-
docker run -p 8080:80 copilot-metrics-viewer
140+
docker run -p 8080:80 --env-file ./.env copilot-metrics-viewer
142141
```
143142
The application will be accessible at http://localhost:8080
144143

144+
## Running with API Proxy
145+
146+
Project can run with an API proxy which hides GitHub tokens and is secure enough to be deployed.
147+
Api Proxy project is in `\api` directory. Vue app makes the calls to `/api/github` which then are proxied to `https://api.github.com` with appropriate bearer token.
148+
149+
Proxy can authenticate user using GitHub App. In order to do that, following environment variables are required:
150+
151+
* `GITHUB_CLIENT_ID` - client Id of the GitHub App registered and installed in the enterprise/org with permissions listed above.
152+
* `GITHUB_CLIENT_SECRET` - client secret of the GitHub App
153+
* `SESSION_SECRET` - random string for securing session state
154+
155+
For local development register `http://localhost:3000/callback` as GH App callback Uri.
156+
For deployed version use the Uri of your app.
157+
158+
To build and run the app with API proxy:
159+
160+
```
161+
docker build -t copilot-metrics-viewer-with-api -f api.Dockerfile .
162+
```
163+
164+
To run:
165+
166+
```
167+
docker run -it --rm -p 8080:3000 --env-file ./.env copilot-metrics-viewer-with-api
168+
```
169+
170+
## Azure Deployment
171+
172+
Application can be deployed using [Azure Developer CLI](https://aka.ms/azd) (azd).
173+
174+
Before running `azd up` configure GitHub variables:
175+
176+
```bash
177+
azd env set VUE_APP_SCOPE <organization/enterprise>
178+
# when using organization
179+
azd env set VUE_APP_GITHUB_ORG <org name>
180+
# when using enterprise
181+
azd env set VUE_APP_GITHUB_ENT <ent name>
182+
azd env set VUE_APP_GITHUB_API_URL /api/github
183+
azd env set GITHUB_CLIENT_ID <client id>
184+
azd env set GITHUB_CLIENT_SECRET <client secret>
185+
```
186+
145187
## License
146188

147189
This project is licensed under the terms of the MIT open source license. Please refer to [MIT](./LICENSE.txt) for the full terms.

api.Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ WORKDIR /app
44
COPY package*.json ./
55
RUN npm install
66
COPY . .
7+
# this will tokenize the app
78
RUN npm run build
89

910
# Stage 2: Prepare the Node.js API
@@ -17,9 +18,14 @@ COPY api/ .
1718

1819
# Copy the built Vue.js app from the previous stage
1920
COPY --from=build-stage /app/dist /api/public
21+
COPY --from=build-stage /app/dist/assets/app-config.js /api/app-config.template.js
22+
23+
# install gettext-base for envsubst
24+
RUN apt-get update && apt-get install -y gettext-base
2025

2126
# Expose the port your API will run on
2227
EXPOSE 3000
2328

2429
# Command to run your API (and serve your Vue.js app)
25-
CMD ["node", "server.mjs"]
30+
RUN chmod +x /api/docker-entrypoint.api/entrypoint.sh
31+
ENTRYPOINT ["/api/docker-entrypoint.api/entrypoint.sh"]

api/.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Client Id from GitHub App installed on the organization
2-
CLIENT_ID=
2+
GITHUB_CLIENT_ID=
33

44
# Client Secret from GitHub App installed on the organization
5-
CLIENT_SECRET=
5+
GITHUB_CLIENT_SECRET=
66

77
# Secret for the session - random string for session
88
SESSION_SECRET=
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
configTemplateFile=/api/app-config.template.js
4+
configTargetFile=/api/public/assets/app-config.js
5+
6+
envsubst <"$configTemplateFile" >"$configTargetFile"
7+
8+
node server.mjs

api/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "gh-api",
33
"version": "1.0.0",
4-
"main": "index.js",
4+
"main": "server.js",
55
"scripts": {
6-
"start": "node index.mjs",
6+
"start": "node server.mjs",
77
"test": "echo \"Error: no test specified\" && exit 1"
88
},
99
"author": "",

api/server.mjs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import path from 'path';
44
import axios from 'axios';
55
import { fileURLToPath } from 'url';
66
import session from 'express-session';
7-
import {createProxyMiddleware} from 'http-proxy-middleware';
8-
9-
dotenv.config({ path: path.join(__dirname, '.env.local') });
7+
import { createProxyMiddleware } from 'http-proxy-middleware';
108

119
// Construct __dirname equivalent in ES module scope
1210
const __dirname = path.dirname(fileURLToPath(import.meta.url));
11+
dotenv.config({ path: path.join(__dirname, '.env') });
12+
//dotenv.config();
1313

1414
const app = express();
1515

@@ -22,11 +22,19 @@ app.use(session({
2222

2323
// Middleware to add Authorization header
2424
const authMiddleware = (req, res, next) => {
25-
if (!req.session.token) {
25+
// not ideal but if someone wanted to use hardcoded token on the backend
26+
if (!req.session.token && !process.env.VUE_APP_GITHUB_TOKEN) {
2627
res.status(401).send('Unauthorized');
2728
return;
2829
}
30+
31+
if (process.env.VUE_APP_GITHUB_TOKEN) {
32+
// Use the hardcoded token if it's available
33+
req.session.token = process.env.VUE_APP_GITHUB_TOKEN;
34+
}
35+
2936
req.headers['Authorization'] = `Bearer ${req.session.token}`;
37+
console.log('Added Authorization to:', req.url);
3038
next();
3139
};
3240

@@ -47,8 +55,8 @@ app.use('/api/github', authMiddleware, githubProxy);
4755

4856
const exchangeCode = async (code) => {
4957
const params = new URLSearchParams({
50-
client_id: process.env.CLIENT_ID,
51-
client_secret: process.env.CLIENT_SECRET,
58+
client_id: process.env.GITHUB_CLIENT_ID,
59+
client_secret: process.env.GITHUB_CLIENT_SECRET,
5260
code: code,
5361
});
5462

@@ -73,11 +81,24 @@ const exchangeCode = async (code) => {
7381
app.use(express.static(path.join(__dirname, 'public')));
7482

7583
app.get('/login', (req, res) => {
76-
res.redirect(`https://github.com/login/oauth/authorize?client_id=${process.env.CLIENT_ID}`);
84+
// build the URL to redirect to GitHub using host and scheme
85+
const redirectUrl = `${req.protocol}://${req.get('host')}/callback`;
86+
// generate random state
87+
// store the state in the session
88+
req.session.state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
89+
90+
res.redirect(`https://github.com/login/oauth/authorize?client_id=${process.env.GITHUB_CLIENT_ID}&redirect_uri=${redirectUrl}&state=${req.session.state}`);
7791
});
7892

7993
app.get('/callback', async (req, res) => {
8094
const code = req.query.code;
95+
const state = req.query.state;
96+
97+
// check the state against the session
98+
if (state !== req.session.state) {
99+
res.send('Invalid state');
100+
return;
101+
}
81102

82103
const tokenData = await exchangeCode(code);
83104

0 commit comments

Comments
 (0)