Skip to content

Commit 5d0aecc

Browse files
feat: admin access to download data (#39)
1 parent e50a0f3 commit 5d0aecc

File tree

8 files changed

+152
-2
lines changed

8 files changed

+152
-2
lines changed

.env.sample

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ APP_ID="11"
22
GITHUB_APP_PRIVATE_KEY_BASE64="base64 encoded private key"
33
PRIVATE_KEY_PATH="very/secure/location/gh_app_key.pem"
44
WEBHOOK_SECRET="secret"
5-
WEBSITE_ADDRESS="https://github.app.home"
5+
WEBSITE_ADDRESS="https://github.app.home"
6+
LOGIN_USER=username
7+
LOGIN_PASSWORD=strongpassword

app.js

+6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ http
187187
case "GET /cla":
188188
routes.cla(req, res);
189189
break;
190+
case "GET /download":
191+
routes.downloadCenter(req, res);
192+
break;
193+
case "POST /download":
194+
routes.download(req, res);
195+
break;
190196
case "POST /cla":
191197
routes.submitCla(req, res, app);
192198
break;

build/docker-compose.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ services:
1010
- APP_ID=11111 # Replace this with your app id. You'll need to create a github app for this: https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app
1111
- GITHUB_APP_PRIVATE_KEY_BASE64=your_github_app_private_key_base64_encode # Replace this. First, get the private key from `GitHub Settings > Developer settings > GitHub Apps > {Your GitHub App} > Private Keys > Click(Generate Private Key)`. And then encode it to base64 using this command: `openssl base64 -in /path/to/original-private-key.pem -out ./base64EncodedKey.txt -A`
1212
- WEBHOOK_SECRET=the_secret_you_configured_for_webhook_in_your_github_app # Replace this
13-
- WEBSITE_ADDRESS=http://localhost:3000 # Replace this with your website domain name. It is recommended to use https, make sure to forward your your traffic on 443 to 3000 port(or whatever you configured earlier in environment.PORT) using reverse proxy such as nginx.
13+
- WEBSITE_ADDRESS=http://localhost:3000 # Replace this with your website domain name. It is recommended to use https, make sure to forward your your traffic on 443 to 3000 port(or whatever you configured earlier in environment.PORT) using reverse proxy such as nginx.
14+
- LOGIN_USER=username # Replace with a memorable username
15+
- LOGIN_PASSWORD=strongpassword # Replace with a strong long password

src/auth.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const loginAttempts = {};
2+
3+
export function isPasswordValid(username, password){
4+
// Check if user has exceeded max attempts
5+
if (loginAttempts[username] >= 3) {
6+
console.error("Account locked! Too many attempts.")
7+
return false
8+
}
9+
// Check credentials
10+
if (process.env.LOGIN_USER === username && process.env.LOGIN_PASSWORD === password) {
11+
// Successful login
12+
loginAttempts[username] = 0; // Reset attempts on successful login
13+
return true
14+
}
15+
loginAttempts[username] = (loginAttempts[username] || 0) + 1;
16+
return false
17+
}

src/helpers.js

+25
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,28 @@ export function isCLASigned(username) {
173173
}
174174
return false;
175175
}
176+
177+
export function jsonToCSV(arr) {
178+
if (!arr || arr.length === 0) return '';
179+
180+
const headers = Object.keys(arr[0]);
181+
const csvRows = [];
182+
183+
// Add headers
184+
csvRows.push(headers.join(','));
185+
186+
// Add rows
187+
for (const row of arr) {
188+
const values = headers.map(header => {
189+
const value = row[header];
190+
// Handle nested objects and arrays
191+
const escaped = typeof value === 'object' && value !== null
192+
? JSON.stringify(value).replace(/"/g, '""')
193+
: String(value).replace(/"/g, '""');
194+
return `"${escaped}"`;
195+
});
196+
csvRows.push(values.join(','));
197+
}
198+
199+
return csvRows.join('\n');
200+
}

src/routes.js

+49
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
parseUrlQueryParams,
88
} from "./helpers.js";
99
import { resolve } from "path";
10+
import { jsonToCSV } from "./helpers.js";
11+
import { isPasswordValid } from "./auth.js";
1012

1113
export const routes = {
1214
home(req, res) {
@@ -75,6 +77,53 @@ export const routes = {
7577
});
7678
},
7779

80+
downloadCenter(req, res) {
81+
const htmlPath = resolve(PROJECT_ROOT_PATH, "views", "download.html");
82+
fs.readFile(htmlPath, function (err, data) {
83+
if (err) {
84+
res.writeHead(404);
85+
res.write("Errors: File not found");
86+
return res.end();
87+
}
88+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
89+
res.write(data);
90+
return res.end();
91+
});
92+
},
93+
94+
download(req, res){
95+
let body = "";
96+
req.on("data", (chunk) => {
97+
body += chunk.toString(); // convert Buffer to string
98+
});
99+
100+
req.on("end", async () => {
101+
let bodyJson = queryStringToJson(body);
102+
if(!bodyJson || !bodyJson['username'] || !bodyJson['password'] || !isPasswordValid(bodyJson['username'], bodyJson['password'])){
103+
res.writeHead(404);
104+
res.write("Not Authorized");
105+
return res.end();
106+
}
107+
const jsonData = storage.get();
108+
const format = bodyJson['format'];
109+
if(format && format==='json'){
110+
// Set headers for JSON file download
111+
res.setHeader('Content-Disposition', 'attachment; filename=data.json');
112+
res.setHeader('Content-Type', 'application/json');
113+
// Convert JavaScript object to JSON string and send
114+
const jsonString = JSON.stringify(jsonData, null, 2);
115+
return res.end(jsonString);
116+
}
117+
// Send as csv format by default
118+
// Set headers for CSV file download
119+
res.setHeader('Content-Disposition', 'attachment; filename=data.csv');
120+
res.setHeader('Content-Type', 'text/csv');
121+
// Convert JavaScript object to CSV and send
122+
const csvString = jsonToCSV(jsonData);
123+
return res.end(csvString);
124+
})
125+
},
126+
78127
default(req, res) {
79128
res.writeHead(404);
80129
res.write("Path not found!");

src/storage.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const storage = {
3333
},
3434
get(filters) {
3535
const currentData = JSON.parse(fs.readFileSync(dbPath, "utf8"));
36+
if(!filters) return currentData;
3637
return currentData.filter((item) => {
3738
for (const [key, value] of Object.entries(filters)) {
3839
if (item[key] !== value) {

views/download.html

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>RudderStack Open Source Contributor Dashboard</title>
7+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css">
8+
<style>
9+
body {
10+
background-color: #F6F5F3;
11+
}
12+
a {
13+
text-decoration: none;
14+
color: "#105ED5";
15+
}
16+
a:visited {
17+
color: "#ADC9FF";
18+
}
19+
</style>
20+
</head>
21+
<body>
22+
<div class="container">
23+
<h2>⬇️ Download Center</h2>
24+
<p>
25+
<br/>
26+
Contact admin for more details
27+
<br/><br/><br/>
28+
<form action="/download" method="post">
29+
<label for="username">Username:</label>
30+
<input type="text" id="username" name="username" required><br><br>
31+
<label for="password">Password:</label>
32+
<input type="password" id="password" name="password" required><br><br>
33+
<label for="format">Format:</label>
34+
<select id="format" name="format">
35+
<option value="csv">CSV</option>
36+
<option value="json">JSON</option>
37+
</select><br><br>
38+
<input type="submit" value="Download">
39+
</form>
40+
<br/><br/><br/><br/><br/>
41+
<hr/>
42+
<ul>
43+
<li><a href="/">Home</a></li>
44+
</ul>
45+
</p>
46+
</div>
47+
</body>
48+
</html>

0 commit comments

Comments
 (0)