Nest intial code

This commit is contained in:
Saif Abdelrazek 2025-05-04 06:15:52 +03:00
commit 7b299ae5b5
8 changed files with 2217 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules
.env.*
todo.txt

76
README.md Normal file
View file

@ -0,0 +1,76 @@
# SaifURL
Welcome to **SaifURL**, my personal URL shortening service! 🚀
## About the Project
SaifURL is a custom-built tool designed exclusively for my own use. It helps me shorten long and complex URLs into concise, manageable links. This service is tailored specifically to my needs, ensuring a streamlined and efficient experience.
### Features
- **Custom Short Links**: Quickly generate short, personal URLs for easier sharing and organization.
- **Simple Design**: A clean and intuitive interface designed just for me.
- **Fast and Reliable**: No delays—shorten URLs instantly.
- **Private Service**: This tool is solely for my personal use and not intended for public access.
### Built With
This project leverages the following technologies:
- **EJS** (Embedded JavaScript): Templating engine for dynamic HTML rendering.
- **JavaScript**: Core functionality and logic.
## Getting Started
Since this service is strictly for my personal use, heres how I set it up for myself:
### Prerequisites
Ensure the following are installed on my system:
- [Node.js](https://nodejs.org/) (Latest LTS version recommended)
- [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/)
### Installation
1. Clone the repository:
```bash
git clone https://github.com/saifabdelrazek011/saifurl.git
```
2. Navigate to the project directory:
```bash
cd saifurl
```
3. Install the dependencies:
```bash
npm install
```
### Usage
1. Start the development server:
```bash
npm start
```
2. Open my browser and visit:
```
http://localhost:3000
```
3. Use the service to shorten my URLs as needed.
## License
This project is licensed under the [MIT License](LICENSE). Since this is a personal tool, I control its usage and modifications.
---
Let me know if you'd like any further changes!

27
modules/shortUrl.js Normal file
View file

@ -0,0 +1,27 @@
import mongoose from "mongoose";
import { nanoid } from "nanoid";
const shortUrlSchema = new mongoose.Schema(
{
full: {
type: String,
required: true,
unique: true,
},
short: {
type: String,
required: true,
unique: true,
default: () => nanoid(6),
},
clicks: {
type: Number,
required: true,
default: 0,
},
},
{ timestamps: true }
);
const ShortUrl = mongoose.model("ShortUrl", shortUrlSchema);
export default ShortUrl;

1609
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "saifurl",
"version": "1.0.0",
"description": "",
"homepage": "https://github.com/SaifAbdelrazek011/saifurl#readme",
"bugs": {
"url": "https://github.com/SaifAbdelrazek011/saifurl/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/SaifAbdelrazek011/saifurl.git"
},
"license": "ISC",
"author": "",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"dotenv": "^16.5.0",
"ejs": "^3.1.10",
"express": "^5.1.0",
"method-override": "^3.0.0",
"mongoose": "^8.14.0",
"nanoid": "^5.1.5",
"shortid": "^2.2.17"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

147
server.js Normal file
View file

@ -0,0 +1,147 @@
import express from "express";
import mongoose from "mongoose";
import dotenv from "dotenv";
import ShortUrl from "./modules/shortUrl.js";
import { nanoid } from "nanoid";
import methodOverride from "method-override";
dotenv.config({ path: ".env.local" });
const MONGODB_URI = process.env.MONGODB_URI;
const PORT = process.env.PORT || 3000;
const accessToken = process.env.ACCESS_TOKEN;
const app = express();
app.use(express.json());
app.use(methodOverride("_method"));
app.use(express.urlencoded({ extended: false }));
// View engine and body parsing
app.set("view engine", "ejs");
// Database connection
mongoose
.connect(MONGODB_URI)
.then(() => console.log("Connected to MongoDB"))
.catch((err) => console.error("Failed to connect to MongoDB", err));
app.get("/", async (req, res) => {
try {
const token = req.query.token;
const shortUrls = await ShortUrl.find();
if (token === accessToken) {
return res.render("admin", { shortUrls: shortUrls, token: token });
}
res.render("viewer", { shortUrls: shortUrls });
} catch (error) {
console.error("Error fetching short URLs:", error);
res.status(500).send("Internal Server Error");
}
});
// CREATE A NEW SHORT URL
app.post("/shorturls", async (req, res) => {
try {
const { fullUrl, token, customShortUrl } = req.body;
const domain = `${req.protocol}://${req.headers.host}`;
if (token !== accessToken) {
return res.status(403).send("Forbidden: Invalid access token");
}
if (!fullUrl) {
return res.status(400).send("Full URL is required.");
}
const short = customShortUrl || nanoid(7);
const existingShort = await ShortUrl.findOne({ short });
if (existingShort) {
return res.status(400).send("Custom short URL already exists.");
}
const existingFull = await ShortUrl.findOne({ full: fullUrl });
if (existingFull) {
return res
.status(400)
.send(
`Full URL already exists at <a href="${domain}/${existingFull.short}">${domain}/${existingFull.short}</a>`
);
}
await ShortUrl.create({ full: fullUrl, short });
res.redirect("/?token=" + token);
} catch (error) {
console.error("Error creating short URL:", error);
res.status(500).send("Internal Server Error");
}
});
// REDIRECT TO THE FULL URL
app.get("/:shorturl", async (req, res) => {
try {
const shortUrl = await ShortUrl.findOne({ short: req.params.shorturl });
if (!shortUrl) return res.sendStatus(404);
shortUrl.clicks++;
await shortUrl.save();
res.redirect(shortUrl.full);
} catch (error) {
console.error("Error fetching short URL:", error);
res.status(500).send("Internal Server Error");
}
});
// UPDATE AN EXISTING SHORT URL
app.patch("/shorturls/:id", async (req, res) => {
const { fullUrl, customShortUrl } = req.body;
const token = req.query.token;
const id = req.params.id;
if (token !== accessToken) {
return res.status(401).json({ success: false, message: "Invalid token" });
}
try {
const updated = await ShortUrl.findByIdAndUpdate(
id,
{ full: fullUrl, short: customShortUrl },
{ new: true }
);
if (!updated) {
return res
.status(404)
.json({ success: false, message: "Short URL not found" });
}
res.json({ success: true, updated });
} catch (err) {
console.error(err);
res.status(500).json({ success: false, message: "Server error" });
}
});
// DELETE A SHORT URL
app.delete("/shorturls/:id", async (req, res) => {
try {
const { id } = req.params;
const { token } = req.query;
if (token !== accessToken) {
return res.status(403).send("Forbidden: Invalid access token");
}
await ShortUrl.findByIdAndDelete(id);
res.redirect("/?token=" + token);
} catch (error) {
console.error("Error deleting short URL:", error);
res.status(500).send("Internal Server Error");
}
});
// START THE SERVER
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

251
views/admin.ejs Normal file
View file

@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
/>
<!-- Bootstrap JS Bundle (includes Popper) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<title>SaifURL Admin</title>
</head>
<body class="bg-light">
<div class="container py-5">
<h1 class="text-center mb-4 fw-bold text-primary">Saif URLs</h1>
<!-- Token input and New URL Button -->
<form
action="/shorturls"
method="POST"
class="my-4 p-5 bg-white shadow-lg rounded"
>
<h2 class="text-center mb-4 fw-bold text-secondary">
Create a Short URL
</h2>
<div class="row g-4">
<!-- Full URL -->
<div class="col-md-6">
<label for="fullUrl" class="form-label fw-bold">Full URL</label>
<input
required
placeholder="Enter the full URL"
type="url"
name="fullUrl"
id="fullUrl"
class="form-control border-secondary"
/>
</div>
<input
type="hidden"
name="token"
id="tokeninput"
value="<%= token %>"
/>
<!-- Custom Short URL -->
<div class="col-md-4">
<label for="customShortUrl" class="form-label fw-bold"
>Custom Short URL
</label>
<input
type="text"
name="customShortUrl"
id="customShortUrl"
placeholder="Enter custom short URL (optional)"
class="form-control border-secondary"
/>
</div>
<!-- Submit Button -->
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-success w-100 fw-bold" type="submit">
Shorten
</button>
</div>
</div>
</form>
<div class="table-responsive shadow-sm rounded bg-white p-4">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Full URL</th>
<th>Short</th>
<th>Clicks</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<% shortUrls.forEach(shortUrl => { %>
<tr>
<td>
<a
href="<%= shortUrl.full %>"
target="_blank"
class="text-decoration-none text-primary fw-bold"
><%= shortUrl.full %></a
>
</td>
<td>
<a
href="<%= shortUrl.short %>"
target="_blank"
class="text-decoration-none text-success fw-bold"
><%= shortUrl.short %></a
>
</td>
<td class="text-center fw-bold"><%= shortUrl.clicks %></td>
<td>
<!-- Edit Button -->
<button
class="btn btn-warning btn-sm me-2 fw-bold"
data-bs-toggle="modal"
data-bs-target="#editModal<%= shortUrl._id %>"
>
Edit
</button>
<!-- Delete Button -->
<button
onclick="deleteUrl('<%= shortUrl._id %>')"
class="btn btn-danger btn-sm"
>
Delete
</button>
</td>
</tr>
<!-- Edit Modal -->
<div
class="modal fade"
id="editModal<%= shortUrl._id %>"
tabindex="-1"
aria-labelledby="editModalLabel<%= shortUrl._id %>"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<form id="editForm<%= shortUrl._id %>">
<div class="modal-header">
<h5
class="modal-title"
id="editModalLabel<%= shortUrl._id %>"
>
Edit URL
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label
for="fullUrl<%= shortUrl._id %>"
class="form-label"
>Full URL</label
>
<input
type="url"
class="form-control"
id="fullUrl<%= shortUrl._id %>"
name="fullUrl"
value="<%= shortUrl.full %>"
required
/>
</div>
<div class="mb-3">
<label
for="customShortUrl<%= shortUrl._id %>"
class="form-label"
>Custom Short URL</label
>
<input
type="text"
class="form-control"
id="customShortUrl<%= shortUrl._id %>"
name="customShortUrl"
value="<%= shortUrl.short %>"
/>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="submit" class="btn btn-primary">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("editForm<%= shortUrl._id %>");
const fullUrlInput = document.getElementById("fullUrl<%= shortUrl._id %>");
const customShortUrlInput = document.getElementById("customShortUrl<%= shortUrl._id %>");
const token = "<%= token %>";
const url = `/shorturls/<%= shortUrl._id %>?token=${token}`;
if (form) {
form.addEventListener("submit", async (e) => {
e.preventDefault();
const fullUrl = fullUrlInput.value;
const customShortUrl = customShortUrlInput.value;
try {
const response = await fetch(url, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ fullUrl, customShortUrl }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Server error while updating.");
}
if (data.success) {
window.location.reload();
} else {
alert("Failed to update URL: " + (data.message || "Unknown error"));
}
} catch (error) {
console.error("Update error:", error);
alert("Something went wrong while updating the URL.");
}
});
}
});
</script>
<% }) %>
</tbody>
</table>
</div>
</div>
<script>
async function deleteUrl(id) {
const token = new URLSearchParams(window.location.search).get("token");
if (!confirm("Delete this URL?")) return;
await fetch(`/shorturls/${id}?token=${token}`, { method: "DELETE" });
window.location.reload(); // Reload after deletion
}
</script>
</body>
</html>

68
views/viewer.ejs Normal file
View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css"
/>
<title>SaifURL Viewer</title>
</head>
<body class="bg-light">
<div class="container py-5">
<!-- Page Title -->
<h1 class="text-center mb-4 fw-bold text-primary">Saif URLs Viewer</h1>
<div class="text-center mt-4">
<p class="mb-2 fw-bold">View the project on:</p>
<a
href="https://github.com/saifabdelrazek011/saifurl"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline-primary btn-sm me-2"
>
GitHub Repository
</a>
</div>
<!-- Table Section -->
<div class="table-responsive shadow-sm rounded bg-white p-4 mt-2">
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Full URL</th>
<th>Short URL</th>
<th>Clicks</th>
</tr>
</thead>
<tbody>
<% shortUrls.forEach(shortUrl => { %>
<tr>
<td>
<a
href="<%= shortUrl.full %>"
target="_blank"
class="text-decoration-none text-primary fw-bold"
><%= shortUrl.full %></a
>
</td>
<td>
<a
href="<%= shortUrl.short %>"
target="_blank"
class="text-decoration-none text-success fw-bold"
><%= shortUrl.short %></a
>
</td>
<td class="text-center fw-bold"><%= shortUrl.clicks %></td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>