Skip to content

Commit

Permalink
update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
nicnocquee committed Nov 22, 2023
1 parent bf52b00 commit 73570d3
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 10 deletions.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# About

This repo contains the basic Next.js project that will allow you to create a public password-protected file download link. The file will be protected using basic authentication.
This repo contains the basic [Next.js](https://nextjs.org) project that will allow you to create a public password-protected file download link. The file will be protected using basic authentication.

Btw, support this repo in Product Hunt, will you? 😁

Expand All @@ -15,11 +15,11 @@ I needed a solution to make a file publicly downloadable, but
- ✅ the files can be directly downloaded, no download page.
- ✅ the files can be downloaded from script or command line.

I couldn't find anything that meets those requirements. So I made this repo by storing the files in private GitHub repository and serving them through Vercel, both of which are FREE.
I couldn't find anything that meets those requirements. So I made this repo by storing the files in private GitHub repository and serving them through [Vercel](https://vercel.com), both of which are FREE.

# Demo

1. Check out [this demo website](https://next-secure-download.vercel.app/) deployed in Vercel.
1. Check out [this demo website](https://next-secure-download.vercel.app/) which was deployed to Vercel.
2. Click the "Download secret file" link.
3. Use `admin` and `supersecret` as User and Password, respectively.

Expand Down Expand Up @@ -47,6 +47,15 @@ curl -OJ "https://admin:supersecret@next-secure-download.vercel.app/api/download

If you'd rather watch a video on how to use this project, check it out in [this blog post](https://nicnocquee.medium.com/create-password-protected-download-links-for-free-with-github-and-vercel-a4758602b21e).

# Use cases

- Securely store and serve a configuration file for your app.
- Share files with others privately.

# Code overview

The not-so-secret sauce is inside the `app/api/download/[file_name]/route.ts` and `middleware.ts`. In `middleware.ts`, the app will check the credentials. In `app/api/download/[file_name]/route.ts`, the app will find and return the file. That's it.

# Notes

1. It goes without saying that the files remain secret as long as your repository is private!
Expand Down
16 changes: 14 additions & 2 deletions app/api/download/[file_name]/route.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,46 @@
import path from "path";
import fs from "fs";

// Function GET exports the downloaded file back to the user.
export const GET = async (
_req: Request,
// Destructure and rename 'file_name' from the params object to 'fileName'
{ params: { file_name: fileName } }: { params: { file_name: string } }
) => {
// If filename is not provided, return 404 error response
if (!fileName) {
return new Response("File not found", { status: 404 });
}

// Decode filename and strip out directory paths for security
const decodedFileName = path.basename(decodeURIComponent(fileName)).trim();
// Ensure that the decodedFileName does not contain any directory traversal characters

// Check for directory traversal characters in the decoded filename to prevent potential malicious access
if (decodedFileName.includes("..")) {
return new Response("Invalid file name", { status: 400 });
}

// Construct the full file path where file is located.
const filePath = path.join(process.cwd(), `./files/${decodedFileName}`);

// Try to access file. If file does not exist or is inaccessible, catch error.
try {
await fs.promises.access(filePath, fs.constants.R_OK);
} catch (err: any) {
// If file is not found, return a 404 error response
if (err.code === "ENOENT") {
return new Response("File not found", { status: 404 });
} else {
console.error("Error accessing file:", err.message);
// Handle other possible errors (e.g., permission issues)
// In case of other errors (e.g., permissions issues), return a 500 error response.
return new Response("Error accessing file", { status: 500 });
}
}

// Read file content for use in response to request
const content = await fs.promises.readFile(filePath);

// Return successful response with file content and appropriate headers to trigger file download
return new Response(content, {
status: 200,
headers: {
Expand Down
28 changes: 23 additions & 5 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,46 @@ export function middleware(req: NextRequest) {
}

// Step 2. Check HTTP Basic Auth header if present
function isAuthenticated(req: NextRequest) {
/**
* isAuthenticated is a function that checks if an incoming request
* is authenticated by verifying the authorization headers are correctly formatted
* and the credentials match a predefined admin user and password.
*
* @param {NextRequest} req - The incoming request object.
* @return {Boolean} True if authenticated, false otherwise.
*/
function isAuthenticated(req: NextRequest): boolean {
//Retrieve the authorization header from the request. It considers both lowercase and proper case 'Authorization' for flexibility.
const authheader =
req.headers.get("authorization") || req.headers.get("Authorization");

//Checks if the authheader variable is null or undefined, meaning the request lacks an authorization header.
if (!authheader) {
return false;
return false; //The request is considered unauthenticated if it lacks an authorization header.
}

// Splitting the 'Bearer' prefix from auth string & encoding it in base64.
const encodedCredentials = authheader.split(" ")[1];
const buffer = Buffer.from(encodedCredentials, "base64");
const decodedCredentials = buffer.toString();

// Identify the index where the user and password are separated.
const colonIndex = decodedCredentials.indexOf(":");

if (colonIndex === -1) {
return false; // Malformed credentials
return false; // Malformed credentials. Invalid credentials format returns false i.e., not authenticated
}

// Extract the user & password from the decoded credentials.
const user = decodedCredentials.substring(0, colonIndex);
const pass = decodedCredentials.substring(colonIndex + 1);

// If user and password match predefined admin credentials, the request is authenticated
if (user === adminUser && pass === adminPassword) {
return true;
return true; //User is authenticated
} else {
return false;
return false; //Incorrect credentials, not authenticated
}
}

Expand Down

1 comment on commit 73570d3

@vercel
Copy link

@vercel vercel bot commented on 73570d3 Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.