Skip to content

Commit

Permalink
feat: admin access to download data (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
gitcommitshow authored Aug 12, 2024
1 parent e50a0f3 commit 5d0aecc
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 2 deletions.
4 changes: 3 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ APP_ID="11"
GITHUB_APP_PRIVATE_KEY_BASE64="base64 encoded private key"
PRIVATE_KEY_PATH="very/secure/location/gh_app_key.pem"
WEBHOOK_SECRET="secret"
WEBSITE_ADDRESS="https://github.app.home"
WEBSITE_ADDRESS="https://github.app.home"
LOGIN_USER=username
LOGIN_PASSWORD=strongpassword
6 changes: 6 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ http
case "GET /cla":
routes.cla(req, res);
break;
case "GET /download":
routes.downloadCenter(req, res);
break;
case "POST /download":
routes.download(req, res);
break;
case "POST /cla":
routes.submitCla(req, res, app);
break;
Expand Down
4 changes: 3 additions & 1 deletion build/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ services:
- 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
- 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`
- WEBHOOK_SECRET=the_secret_you_configured_for_webhook_in_your_github_app # Replace this
- 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.
- 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.
- LOGIN_USER=username # Replace with a memorable username
- LOGIN_PASSWORD=strongpassword # Replace with a strong long password
17 changes: 17 additions & 0 deletions src/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const loginAttempts = {};

export function isPasswordValid(username, password){
// Check if user has exceeded max attempts
if (loginAttempts[username] >= 3) {
console.error("Account locked! Too many attempts.")
return false
}
// Check credentials
if (process.env.LOGIN_USER === username && process.env.LOGIN_PASSWORD === password) {
// Successful login
loginAttempts[username] = 0; // Reset attempts on successful login
return true
}
loginAttempts[username] = (loginAttempts[username] || 0) + 1;
return false
}
25 changes: 25 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,28 @@ export function isCLASigned(username) {
}
return false;
}

export function jsonToCSV(arr) {
if (!arr || arr.length === 0) return '';

const headers = Object.keys(arr[0]);
const csvRows = [];

// Add headers
csvRows.push(headers.join(','));

// Add rows
for (const row of arr) {
const values = headers.map(header => {
const value = row[header];
// Handle nested objects and arrays
const escaped = typeof value === 'object' && value !== null
? JSON.stringify(value).replace(/"/g, '""')
: String(value).replace(/"/g, '""');
return `"${escaped}"`;
});
csvRows.push(values.join(','));
}

return csvRows.join('\n');
}
49 changes: 49 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
parseUrlQueryParams,
} from "./helpers.js";
import { resolve } from "path";
import { jsonToCSV } from "./helpers.js";
import { isPasswordValid } from "./auth.js";

export const routes = {
home(req, res) {
Expand Down Expand Up @@ -75,6 +77,53 @@ export const routes = {
});
},

downloadCenter(req, res) {
const htmlPath = resolve(PROJECT_ROOT_PATH, "views", "download.html");
fs.readFile(htmlPath, function (err, data) {
if (err) {
res.writeHead(404);
res.write("Errors: File not found");
return res.end();
}
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.write(data);
return res.end();
});
},

download(req, res){
let body = "";
req.on("data", (chunk) => {
body += chunk.toString(); // convert Buffer to string
});

req.on("end", async () => {
let bodyJson = queryStringToJson(body);
if(!bodyJson || !bodyJson['username'] || !bodyJson['password'] || !isPasswordValid(bodyJson['username'], bodyJson['password'])){
res.writeHead(404);
res.write("Not Authorized");
return res.end();
}
const jsonData = storage.get();
const format = bodyJson['format'];
if(format && format==='json'){
// Set headers for JSON file download
res.setHeader('Content-Disposition', 'attachment; filename=data.json');
res.setHeader('Content-Type', 'application/json');
// Convert JavaScript object to JSON string and send
const jsonString = JSON.stringify(jsonData, null, 2);
return res.end(jsonString);
}
// Send as csv format by default
// Set headers for CSV file download
res.setHeader('Content-Disposition', 'attachment; filename=data.csv');
res.setHeader('Content-Type', 'text/csv');
// Convert JavaScript object to CSV and send
const csvString = jsonToCSV(jsonData);
return res.end(csvString);
})
},

default(req, res) {
res.writeHead(404);
res.write("Path not found!");
Expand Down
1 change: 1 addition & 0 deletions src/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const storage = {
},
get(filters) {
const currentData = JSON.parse(fs.readFileSync(dbPath, "utf8"));
if(!filters) return currentData;
return currentData.filter((item) => {
for (const [key, value] of Object.entries(filters)) {
if (item[key] !== value) {
Expand Down
48 changes: 48 additions & 0 deletions views/download.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RudderStack Open Source Contributor Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css">
<style>
body {
background-color: #F6F5F3;
}
a {
text-decoration: none;
color: "#105ED5";
}
a:visited {
color: "#ADC9FF";
}
</style>
</head>
<body>
<div class="container">
<h2>⬇️ Download Center</h2>
<p>
<br/>
Contact admin for more details
<br/><br/><br/>
<form action="/download" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required><br><br>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required><br><br>
<label for="format">Format:</label>
<select id="format" name="format">
<option value="csv">CSV</option>
<option value="json">JSON</option>
</select><br><br>
<input type="submit" value="Download">
</form>
<br/><br/><br/><br/><br/>
<hr/>
<ul>
<li><a href="/">Home</a></li>
</ul>
</p>
</div>
</body>
</html>

0 comments on commit 5d0aecc

Please sign in to comment.