diff --git a/public/styles.css b/public/styles.css index dad1c46..eda73f3 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,32 +1,32 @@ -@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800;900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800;900&display=swap"); :root { - --primary-color: #FF6B35; - --primary-dark: #E85A2D; - --secondary-color: #FF9B70; - --secondary-dark: #FF8555; - --background-color: #FFF8F5; + --primary-color: #ff6b35; + --primary-dark: #e85a2d; + --secondary-color: #ff9b70; + --secondary-dark: #ff8555; + --background-color: #fff8f5; --panel-color: #ffffff; --text-color: #333333; --text-light: #ffffff; - --border-color: #FFD0B8; - --success-color: #4CAF50; - --error-color: #F44336; + --border-color: #ffd0b8; + --success-color: #4caf50; + --error-color: #f44336; --border-radius: 12px; --transition: all 0.3s ease; } /* Dark mode variables */ .dark-mode { - --primary-color: #FF8C5A; - --primary-dark: #FF7F4D; - --secondary-color: #FFB38F; - --secondary-dark: #FFA67F; - --background-color: #1E1E1E; - --panel-color: #2D2D2D; - --text-color: #E0E0E0; - --text-light: #FFFFFF; - --border-color: #4D4D4D; + --primary-color: #ff8c5a; + --primary-dark: #ff7f4d; + --secondary-color: #ffb38f; + --secondary-dark: #ffa67f; + --background-color: #1e1e1e; + --panel-color: #2d2d2d; + --text-color: #e0e0e0; + --text-light: #ffffff; + --border-color: #4d4d4d; } * { @@ -36,13 +36,42 @@ } body { - font-family: 'Plus Jakarta Sans', sans-serif; + font-family: "Plus Jakarta Sans", sans-serif; background-color: var(--background-color); color: var(--text-color); line-height: 1.6; font-size: 16px; } +/* FAQ page styles */ +/* Add styles for FAQ page elements here */ + +.faq-container { + margin: 20px; +} + +.faq-question { + font-weight: 600; + margin-bottom: 10px; +} + +.faq-answer { + margin-bottom: 20px; +} + +/* Responsive design for FAQ page */ +@media (max-width: 768px) { + .faq-container { + padding: 10px; + } + + .faq-question { + font-size: 18px; + } + .faq-answer { + font-size: 16px; + } +} .app-container { display: flex; min-height: 100vh; @@ -99,7 +128,9 @@ body { overflow-y: auto; } -h1, h2, h3 { +h1, +h2, +h3 { margin-bottom: 25px; font-weight: 900; } @@ -188,7 +219,7 @@ select { margin-bottom: 20px; border: 2px solid var(--border-color); border-radius: var(--border-radius); - font-family: 'Plus Jakarta Sans', sans-serif; + font-family: "Plus Jakarta Sans", sans-serif; font-size: 16px; transition: var(--transition); } @@ -457,7 +488,7 @@ select:focus { .progress-bar { width: 100%; height: 8px; - background-color: #FFE0D0; + background-color: #ffe0d0; border-radius: 4px; overflow: hidden; } @@ -546,7 +577,6 @@ select:focus { .main-title { font-size: 36px; } - } .hero-section { @@ -596,6 +626,10 @@ select:focus { display: flex; gap: 20px; } +.cta-buttons-primary { + display: flex; + justify-content: space-between; +} .features-section { margin-bottom: 60px; @@ -655,7 +689,7 @@ select:focus { } .step:not(:last-child)::after { - content: ''; + content: ""; position: absolute; top: 30px; right: -15px; @@ -772,27 +806,55 @@ select:focus { text-align: center; } -.mt-1 { margin-top: 0.25rem; } -.mt-2 { margin-top: 0.5rem; } -.mt-3 { margin-top: 1rem; } -.mt-4 { margin-top: 1.5rem; } -.mt-5 { margin-top: 3rem; } +.mt-1 { + margin-top: 0.25rem; +} +.mt-2 { + margin-top: 0.5rem; +} +.mt-3 { + margin-top: 1rem; +} +.mt-4 { + margin-top: 1.5rem; +} +.mt-5 { + margin-top: 3rem; +} -.mb-1 { margin-bottom: 0.25rem; } -.mb-2 { margin-bottom: 0.5rem; } -.mb-3 { margin-bottom: 1rem; } -.mb-4 { margin-bottom: 1.5rem; } -.mb-5 { margin-bottom: 3rem; } +.mb-1 { + margin-bottom: 0.25rem; +} +.mb-2 { + margin-bottom: 0.5rem; +} +.mb-3 { + margin-bottom: 1rem; +} +.mb-4 { + margin-bottom: 1.5rem; +} +.mb-5 { + margin-bottom: 3rem; +} .mx-auto { margin-left: auto; margin-right: auto; } -.w-full { width: 100%; } -.max-w-md { max-width: 28rem; } -.max-w-lg { max-width: 32rem; } -.max-w-xl { max-width: 36rem; } +.w-full { + width: 100%; +} +.max-w-md { + max-width: 28rem; +} +.max-w-lg { + max-width: 32rem; +} +.max-w-xl { + max-width: 36rem; +} /* Print styles */ @media print { @@ -864,6 +926,11 @@ select:focus { border-radius: var(--border-radius); transition: var(--transition); } +@media (max-width: 769px) { + .cta-buttons { + max-width: 300px; + } +} @media screen and (max-width: 768px) { .dashboard-stats { @@ -1329,4 +1396,29 @@ canvas { .content { margin-left: 0; } -} \ No newline at end of file +} + +.faq-question { + cursor: pointer; +} +.faq-question:hover { + color: var(--primary-color); + text-decoration: underline; +} +.faq-answer { + display: none; + opacity: 0; + transition: opacity 0.3s ease; + font-family: "Plus Jakarta Sans", sans-serif; + background-color: var(--background-color); + color: var(--text-color); + line-height: 1.6; + font-size: 18px; +} + +.show-answer { + display: block; + margin-top: 10px; + font-style: italic; + opacity: 1; +} diff --git a/server.js b/server.js index 785d26f..699687d 100644 --- a/server.js +++ b/server.js @@ -1,20 +1,20 @@ -const express = require('express'); -const session = require('express-session'); -const passport = require('passport'); -const LocalStrategy = require('passport-local').Strategy; -const bcrypt = require('bcrypt'); -const mongoose = require('mongoose'); -const path = require('path'); -const nodemailer = require('nodemailer'); -const shortid = require('shortid'); -const expressLayouts = require('express-ejs-layouts'); -const dotenv = require('dotenv'); -const crypto = require('crypto'); -const axios = require('axios'); -const QRCode = require('qrcode'); -const useragent = require('express-useragent'); -const geoip = require('geoip-lite'); -const flash = require('connect-flash'); +const express = require("express"); +const session = require("express-session"); +const passport = require("passport"); +const LocalStrategy = require("passport-local").Strategy; +const bcrypt = require("bcrypt"); +const mongoose = require("mongoose"); +const path = require("path"); +const nodemailer = require("nodemailer"); +const shortid = require("shortid"); +const expressLayouts = require("express-ejs-layouts"); +const dotenv = require("dotenv"); +const crypto = require("crypto"); +const axios = require("axios"); +const QRCode = require("qrcode"); +const useragent = require("express-useragent"); +const geoip = require("geoip-lite"); +const flash = require("connect-flash"); dotenv.config(); const app = express(); @@ -24,8 +24,10 @@ const port = process.env.PORT || 3000; console.log(new Date().toISOString()); // MongoDB connection -mongoose.connect(process.env.MongoURI).then(() => console.log('MongoDB connected...')) - .catch(err => console.error('MongoDB connection error:', err)); +mongoose + .connect(process.env.MongoURI) + .then(() => console.log("MongoDB connected...")) + .catch((err) => console.error("MongoDB connection error:", err)); // Define MongoDB schemas and models const userSchema = new mongoose.Schema({ @@ -35,11 +37,11 @@ const userSchema = new mongoose.Schema({ verification_token: String, reset_token: String, reset_token_expires: Date, - created_at: { type: Date, default: Date.now } + created_at: { type: Date, default: Date.now }, }); const urlSchema = new mongoose.Schema({ - user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + user_id: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, original_url: { type: String, required: true }, short_code: { type: String, unique: true, required: true }, custom_alias: { type: String, unique: true, sparse: true }, @@ -49,41 +51,43 @@ const urlSchema = new mongoose.Schema({ whitelist_mode: { type: Boolean, default: false }, allowed_countries: [String], blocked_countries: [String], - password: String + password: String, }); const clickSchema = new mongoose.Schema({ - url_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Url', required: true }, + url_id: { type: mongoose.Schema.Types.ObjectId, ref: "Url", required: true }, clicked_at: { type: Date, default: Date.now }, country: String, browser: String, - device: String + device: String, }); const failedAttemptSchema = new mongoose.Schema({ - url_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Url', required: true }, + url_id: { type: mongoose.Schema.Types.ObjectId, ref: "Url", required: true }, attempted_at: { type: Date, default: Date.now }, - ip_address: String + ip_address: String, }); -const User = mongoose.model('User', userSchema); -const Url = mongoose.model('Url', urlSchema); -const Click = mongoose.model('Click', clickSchema); -const FailedAttempt = mongoose.model('FailedAttempt', failedAttemptSchema); +const User = mongoose.model("User", userSchema); +const Url = mongoose.model("Url", urlSchema); +const Click = mongoose.model("Click", clickSchema); +const FailedAttempt = mongoose.model("FailedAttempt", failedAttemptSchema); // Middleware -app.set('view engine', 'ejs'); -app.set('views', path.join(__dirname, 'views')); +app.set("view engine", "ejs"); +app.set("views", path.join(__dirname, "views")); app.use(expressLayouts); -app.set('layout', 'layout'); -app.use(express.static(path.join(__dirname, 'public'))); +app.set("layout", "layout"); +app.use(express.static(path.join(__dirname, "public"))); app.use(express.urlencoded({ extended: true })); app.use(express.json()); -app.use(session({ - secret: process.env.SESSION_SECRET || 'your-secret-key', - resave: false, - saveUninitialized: false -})); +app.use( + session({ + secret: process.env.SESSION_SECRET || "your-secret-key", + resave: false, + saveUninitialized: false, + }) +); app.use(passport.initialize()); app.use(passport.session()); app.use(useragent.express()); @@ -94,7 +98,7 @@ async function deleteExpiredUrls() { const now = new Date(); try { const expiredUrls = await Url.find({ - auto_delete_at: { $lte: now, $ne: null } + auto_delete_at: { $lte: now, $ne: null }, }); for (const url of expiredUrls) { @@ -103,9 +107,9 @@ async function deleteExpiredUrls() { await Url.findByIdAndDelete(url._id); } - console.log('Expired URLs and associated data deleted successfully'); + console.log("Expired URLs and associated data deleted successfully"); } catch (error) { - console.error('Error deleting expired URLs:', error); + console.error("Error deleting expired URLs:", error); } } @@ -114,26 +118,32 @@ setInterval(deleteExpiredUrls, 60000); // Logging function function log(message, data = {}) { - console.log(JSON.stringify({ timestamp: new Date().toISOString(), message, ...data })); + console.log( + JSON.stringify({ timestamp: new Date().toISOString(), message, ...data }) + ); } // Passport configuration -passport.use(new LocalStrategy( - { usernameField: 'email' }, - async (email, password, done) => { - try { - const user = await User.findOne({ email: email }); - if (!user) return done(null, false, { message: 'Incorrect email.' }); - if (!user.verified) return done(null, false, { message: 'Email not verified.' }); - - const isMatch = await bcrypt.compare(password, user.password); - if (!isMatch) return done(null, false, { message: 'Incorrect password.' }); - return done(null, user); - } catch (err) { - return done(err); +passport.use( + new LocalStrategy( + { usernameField: "email" }, + async (email, password, done) => { + try { + const user = await User.findOne({ email: email }); + if (!user) return done(null, false, { message: "Incorrect email." }); + if (!user.verified) + return done(null, false, { message: "Email not verified." }); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) + return done(null, false, { message: "Incorrect password." }); + return done(null, user); + } catch (err) { + return done(err); + } } - } -)); + ) +); passport.serializeUser((user, done) => { done(null, user.id); @@ -150,95 +160,100 @@ passport.deserializeUser(async (id, done) => { // Nodemailer configuration const transporter = nodemailer.createTransport({ - host: 'smtp.office365.com', + host: process.env.host, port: 587, secure: false, auth: { user: process.env.login, - pass: process.env.password - } + pass: process.env.password, + }, }); -console.log('Verifying transporter connection...'); -transporter.verify(function(error, success) { +console.log("Verifying transporter connection..."); +transporter.verify(function (error, success) { if (error) { - console.log('Transporter verification error:', error); + console.log("Transporter verification error:", error); } else { - console.log('Transporter is ready to send emails'); + console.log("Transporter is ready to send emails"); } }); // Routes -app.get('/', (req, res) => { - res.render('index', { user: req.user }); +app.get("/", (req, res) => { + res.render("index", { user: req.user }); }); -app.get('/logout', (req, res) => { +app.get("/logout", (req, res) => { req.logout(() => { - res.redirect('/'); + res.redirect("/"); }); }); -app.get('/login', (req, res) => { - res.render('login', { message: req.flash('error') }); +app.get("/login", (req, res) => { + res.render("login", { message: req.flash("error") }); }); -app.post('/login', passport.authenticate('local', { - successRedirect: '/dashboard', - failureRedirect: '/login', - failureFlash: true -})); - -app.get('/register', (req, res) => { - res.render('register', { message: req.flash('error') }); +app.post( + "/login", + passport.authenticate("local", { + successRedirect: "/dashboard", + failureRedirect: "/login", + failureFlash: true, + }) +); + +app.get("/register", (req, res) => { + res.render("register", { message: req.flash("error") }); }); -app.post('/register', async (req, res) => { +app.post("/register", async (req, res) => { const { email, password } = req.body; try { const existingUser = await User.findOne({ email: email }); if (existingUser) { - req.flash('error', 'Email already exists'); - return res.redirect('/register'); + req.flash("error", "Email already exists"); + return res.redirect("/register"); } const hashedPassword = await bcrypt.hash(password, 10); - const verificationToken = crypto.randomBytes(32).toString('hex'); - + const verificationToken = crypto.randomBytes(32).toString("hex"); + const newUser = new User({ email: email, password: hashedPassword, verified: false, - verification_token: verificationToken + verification_token: verificationToken, }); await newUser.save(); - + // Send verification email // get the actual base URL from the request - const baseUrl = `${req.protocol}://${req.get('host')}`; + const baseUrl = `${req.protocol}://${req.get("host")}`; const verificationLink = `${baseUrl}/verify/${verificationToken}`; const mailOptions = { from: process.env.login, to: email, - subject: 'Verify your email for URL Slicer', - text: `Please click on this link to verify your email: ${verificationLink}` + subject: "Verify your email for URL Slicer", + text: `Please click on this link to verify your email: ${verificationLink}`, }; - + await transporter.sendMail(mailOptions); - res.redirect('/register-confirmation'); + res.redirect("/register-confirmation"); } catch (error) { console.log(error); - req.flash('error', 'Error registering user'); - res.redirect('/register'); + req.flash("error", "Error registering user"); + res.redirect("/register"); } }); - -app.get('/register-confirmation', (req, res) => { - res.render('register-confirmation'); +app.get("/faq", (req, res) => { + res.render("faq"); +}); +app.get("/register-confirmation", (req, res) => { + res.render("register-confirmation"); }); -app.get('/verify/:token', async (req, res) => { +app.get("/verify/:token", async (req, res) => { const { token } = req.params; try { const user = await User.findOneAndUpdate( @@ -247,49 +262,62 @@ app.get('/verify/:token', async (req, res) => { { new: true } ); if (!user) { - return res.send('Invalid verification token'); + return res.send("Invalid verification token"); } - res.render('verification-success'); + res.render("verification-success"); } catch (error) { console.error(error); - res.send('Error verifying email'); + res.send("Error verifying email"); } }); -app.get('/dashboard', async (req, res) => { +app.get("/dashboard", async (req, res) => { if (!req.user) { - return res.redirect('/login'); + return res.redirect("/login"); } try { const urls = await Url.find({ user_id: req.user._id }); - res.render('dashboard', { user: req.user, urls: urls }); + res.render("dashboard", { user: req.user, urls: urls }); } catch (error) { console.error(error); - res.status(500).send('Error fetching URLs'); + res.status(500).send("Error fetching URLs"); } }); -app.post('/shorten', async (req, res) => { +app.post("/shorten", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } - const { originalUrl, maxUses, autoDeleteAt, whitelistMode, allowedCountries, blockedCountries, customAlias, password } = req.body; + const { + originalUrl, + maxUses, + autoDeleteAt, + whitelistMode, + allowedCountries, + blockedCountries, + customAlias, + password, + } = req.body; const shortCode = customAlias || shortid.generate(); // Check if the custom alias is valid (3-50 symbols) if (customAlias && (customAlias.length < 3 || customAlias.length > 50)) { - return res.status(400).json({ error: 'Custom alias must be between 3 and 50 symbols' }); + return res + .status(400) + .json({ error: "Custom alias must be between 3 and 50 symbols" }); } try { // Check if the custom alias or short code is already taken const existingUrl = await Url.findOne({ - $or: [{ short_code: shortCode }, { custom_alias: customAlias }] + $or: [{ short_code: shortCode }, { custom_alias: customAlias }], }); if (existingUrl) { - return res.status(400).json({ error: 'The custom alias or generated short code is already taken' }); + return res.status(400).json({ + error: "The custom alias or generated short code is already taken", + }); } // If the alias is not taken, create the URL @@ -303,90 +331,110 @@ app.post('/shorten', async (req, res) => { whitelist_mode: whitelistMode, allowed_countries: allowedCountries, blocked_countries: blockedCountries, - password: password + password: password, }); await newUrl.save(); res.json({ shortCode: shortCode, customAlias: customAlias }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error creating shortened URL' }); + res.status(500).json({ error: "Error creating shortened URL" }); } }); // New routes for password reset -app.get('/forgot-password', (req, res) => { - res.render('forgot-password'); +app.get("/forgot-password", (req, res) => { + res.render("forgot-password"); }); -app.post('/forgot-password', async (req, res) => { +app.post("/forgot-password", async (req, res) => { const { email } = req.body; try { const user = await User.findOne({ email: email }); if (!user) { - return res.render('forgot-password', { error: 'No account with that email address exists.' }); + return res.render("forgot-password", { + error: "No account with that email address exists.", + }); } - const resetToken = crypto.randomBytes(20).toString('hex'); + const resetToken = crypto.randomBytes(20).toString("hex"); const resetTokenExpires = Date.now() + 24 * 60 * 60 * 1000; // 24 hours from now user.reset_token = resetToken; user.reset_token_expires = resetTokenExpires; await user.save(); - const resetUrl = `${req.protocol}://${req.get('host')}/reset-password/${resetToken}`; + const resetUrl = `${req.protocol}://${req.get( + "host" + )}/reset-password/${resetToken}`; const mailOptions = { from: process.env.login, to: user.email, - subject: 'Password Reset for URL Slicer', + subject: "Password Reset for URL Slicer", text: `You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n Please click on the following link, or paste this into your browser to complete the process:\n\n ${resetUrl}\n\n - If you did not request this, please ignore this email and your password will remain unchanged.\n` + If you did not request this, please ignore this email and your password will remain unchanged.\n`, }; await transporter.sendMail(mailOptions); - res.render('forgot-password', { message: 'An email has been sent to ' + user.email + ' with further instructions.' }); + res.render("forgot-password", { + message: + "An email has been sent to " + + user.email + + " with further instructions.", + }); } catch (error) { console.error(error); - res.render('forgot-password', { error: 'An error occurred while sending the email. Please try again.' }); + res.render("forgot-password", { + error: "An error occurred while sending the email. Please try again.", + }); } }); -app.get('/reset-password/:token', async (req, res) => { +app.get("/reset-password/:token", async (req, res) => { const { token } = req.params; try { const user = await User.findOne({ reset_token: token, - reset_token_expires: { $gt: Date.now() } + reset_token_expires: { $gt: Date.now() }, }); if (!user) { - return res.render('reset-password', { error: 'Password reset token is invalid or has expired.' }); + return res.render("reset-password", { + error: "Password reset token is invalid or has expired.", + }); } - res.render('reset-password', { token }); + res.render("reset-password", { token }); } catch (error) { console.error(error); - res.render('reset-password', { error: 'An error occurred. Please try again.' }); + res.render("reset-password", { + error: "An error occurred. Please try again.", + }); } }); -app.post('/reset-password', async (req, res) => { +app.post("/reset-password", async (req, res) => { const { token, password, confirmPassword } = req.body; if (password !== confirmPassword) { - return res.render('reset-password', { token, error: 'Passwords do not match.' }); + return res.render("reset-password", { + token, + error: "Passwords do not match.", + }); } try { const user = await User.findOne({ reset_token: token, - reset_token_expires: { $gt: Date.now() } + reset_token_expires: { $gt: Date.now() }, }); if (!user) { - return res.render('reset-password', { error: 'Password reset token is invalid or has expired.' }); + return res.render("reset-password", { + error: "Password reset token is invalid or has expired.", + }); } const hashedPassword = await bcrypt.hash(password, 10); @@ -395,54 +443,84 @@ app.post('/reset-password', async (req, res) => { user.reset_token_expires = undefined; await user.save(); - res.redirect('/login'); + res.redirect("/login"); } catch (error) { console.error(error); - res.render('reset-password', { error: 'An error occurred. Please try again.' }); + res.render("reset-password", { + error: "An error occurred. Please try again.", + }); } }); // Add the new route for the analytics page -app.get('/analytics', (req, res) => { +app.get("/analytics", (req, res) => { if (!req.user) { - return res.redirect('/login'); + return res.redirect("/login"); } - res.render('analytics', { user: req.user }); + res.render("analytics", { user: req.user }); }); -app.get('/api/analytics', async (req, res) => { +app.get("/api/analytics", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } try { - const totalClicks = await Click.countDocuments({ url_id: { $in: await Url.find({ user_id: req.user._id }).distinct('_id') } }); + const totalClicks = await Click.countDocuments({ + url_id: { + $in: await Url.find({ user_id: req.user._id }).distinct("_id"), + }, + }); const totalUrls = await Url.countDocuments({ user_id: req.user._id }); const averageCTR = totalUrls > 0 ? totalClicks / totalUrls : 0; - + const ctrOverTime = await Click.aggregate([ - { $match: { url_id: { $in: await Url.find({ user_id: req.user._id }).distinct('_id') } } }, - { $group: { - _id: { $dateToString: { format: "%Y-%m-%d", date: "$clicked_at" } }, - clicks: { $sum: 1 } - }}, - { $sort: { _id: 1 } } + { + $match: { + url_id: { + $in: await Url.find({ user_id: req.user._id }).distinct("_id"), + }, + }, + }, + { + $group: { + _id: { $dateToString: { format: "%Y-%m-%d", date: "$clicked_at" } }, + clicks: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, ]); const deviceStats = await Click.aggregate([ - { $match: { url_id: { $in: await Url.find({ user_id: req.user._id }).distinct('_id') } } }, - { $group: { - _id: "$device", - count: { $sum: 1 } - }} + { + $match: { + url_id: { + $in: await Url.find({ user_id: req.user._id }).distinct("_id"), + }, + }, + }, + { + $group: { + _id: "$device", + count: { $sum: 1 }, + }, + }, ]); const browserStats = await Click.aggregate([ - { $match: { url_id: { $in: await Url.find({ user_id: req.user._id }).distinct('_id') } } }, - { $group: { - _id: "$browser", - count: { $sum: 1 } - }} + { + $match: { + url_id: { + $in: await Url.find({ user_id: req.user._id }).distinct("_id"), + }, + }, + }, + { + $group: { + _id: "$browser", + count: { $sum: 1 }, + }, + }, ]); res.json({ @@ -451,55 +529,55 @@ app.get('/api/analytics', async (req, res) => { averageCTR, ctrOverTime, deviceStats, - browserStats + browserStats, }); } catch (error) { - console.error('Error fetching analytics data:', error); - res.status(500).json({ error: 'Internal server error' }); + console.error("Error fetching analytics data:", error); + res.status(500).json({ error: "Internal server error" }); } }); -app.get('/api/country-stats/:code', async (req, res) => { +app.get("/api/country-stats/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; try { const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } const countryStats = await Click.aggregate([ { $match: { url_id: url._id } }, { $group: { _id: "$country", count: { $sum: 1 } } }, - { $sort: { count: -1 } } + { $sort: { count: -1 } }, ]); res.json(countryStats); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching country statistics' }); + res.status(500).json({ error: "Error fetching country statistics" }); } }); // Add this new route for the account management page -app.get('/account', (req, res) => { +app.get("/account", (req, res) => { if (!req.user) { - return res.redirect('/login'); + return res.redirect("/login"); } - res.render('account', { user: req.user }); + res.render("account", { user: req.user }); }); // Add this new route for changing the password -app.post('/account/change-password', async (req, res) => { +app.post("/account/change-password", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { currentPassword, newPassword } = req.body; @@ -508,24 +586,26 @@ app.post('/account/change-password', async (req, res) => { const user = await User.findById(req.user._id); const isMatch = await bcrypt.compare(currentPassword, user.password); if (!isMatch) { - return res.status(400).json({ error: 'Current password is incorrect' }); + return res.status(400).json({ error: "Current password is incorrect" }); } const hashedPassword = await bcrypt.hash(newPassword, 10); user.password = hashedPassword; await user.save(); - res.json({ message: 'Password changed successfully' }); + res.json({ message: "Password changed successfully" }); } catch (error) { - console.error('Error changing password:', error); - res.status(500).json({ error: 'An error occurred while changing the password' }); + console.error("Error changing password:", error); + res + .status(500) + .json({ error: "An error occurred while changing the password" }); } }); // Add this new route for deleting the account -app.post('/account/delete', async (req, res) => { +app.post("/account/delete", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } try { @@ -541,80 +621,92 @@ app.post('/account/delete', async (req, res) => { await User.findByIdAndDelete(req.user._id); req.logout(() => { - res.json({ message: 'Account deleted successfully' }); + res.json({ message: "Account deleted successfully" }); }); } catch (error) { - console.error('Error deleting account:', error); - res.status(500).json({ error: 'An error occurred while deleting the account' }); + console.error("Error deleting account:", error); + res + .status(500) + .json({ error: "An error occurred while deleting the account" }); } }); // Add this function to get the client's IP address function getClientIp(req) { - return req.headers['x-forwarded-for']?.split(',').shift() - || req.socket?.remoteAddress; + return ( + req.headers["x-forwarded-for"]?.split(",").shift() || + req.socket?.remoteAddress + ); } -app.get('/:code', async (req, res) => { +app.get("/:code", async (req, res) => { const { code } = req.params; - log('Accessing URL', { code }); + log("Accessing URL", { code }); try { - const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }] }); + const url = await Url.findOne({ + $or: [{ short_code: code }, { custom_alias: code }], + }); if (!url) { - log('URL not found', { code }); - return res.status(404).render('url-not-found'); + log("URL not found", { code }); + return res.status(404).render("url-not-found"); } - log('URL found', { url }); + log("URL found", { url }); // Check if the URL is password protected if (url.password) { // Render the password entry page - return res.render('password-entry', { code: code }); + return res.render("password-entry", { code: code }); } const ip = getClientIp(req); - log('Client IP', { ip }); + log("Client IP", { ip }); - let country = 'Unknown'; + let country = "Unknown"; const geo = geoip.lookup(ip); if (geo) { country = geo.country; - log('Country detected', { country, ip }); + log("Country detected", { country, ip }); } else { - log('Failed to detect country with geoip, trying external API', { ip }); + log("Failed to detect country with geoip, trying external API", { ip }); try { const response = await axios.get(`http://ip-api.com/json/${ip}`); country = response.data.countryCode; - log('Country detected from external API', { country }); + log("Country detected from external API", { country }); } catch (apiError) { - log('Failed to detect country from external API', { error: apiError.message }); + log("Failed to detect country from external API", { + error: apiError.message, + }); } } if (url.whitelist_mode) { - log('Whitelist mode', { allowedCountries: url.allowed_countries }); + log("Whitelist mode", { allowedCountries: url.allowed_countries }); if (!url.allowed_countries.includes(country)) { - log('Access denied: country not in whitelist', { country }); - return res.status(403).render('access-denied'); + log("Access denied: country not in whitelist", { country }); + return res.status(403).render("access-denied"); } } else { - log('Blacklist mode', { blockedCountries: url.blocked_countries }); + log("Blacklist mode", { blockedCountries: url.blocked_countries }); if (url.blocked_countries.includes(country)) { - log('Access denied: country in blocklist', { country }); - return res.status(403).render('access-denied'); + log("Access denied: country in blocklist", { country }); + return res.status(403).render("access-denied"); } } const clickCount = await Click.countDocuments({ url_id: url._id }); - log('Current click count', { clickCount, maxUses: url.max_uses }); + log("Current click count", { clickCount, maxUses: url.max_uses }); - if (url.max_uses !== null && url.max_uses > 0 && clickCount >= url.max_uses) { - log('Max uses reached', { clickCount, maxUses: url.max_uses }); - return res.status(410).render('max-uses-reached'); + if ( + url.max_uses !== null && + url.max_uses > 0 && + clickCount >= url.max_uses + ) { + log("Max uses reached", { clickCount, maxUses: url.max_uses }); + return res.status(410).render("max-uses-reached"); } const userAgent = req.useragent; @@ -622,31 +714,58 @@ app.get('/:code', async (req, res) => { url_id: url._id, country: country, browser: userAgent.browser, - device: userAgent.isMobile ? 'Mobile' : (userAgent.isTablet ? 'Tablet' : 'Desktop') + device: userAgent.isMobile + ? "Mobile" + : userAgent.isTablet + ? "Tablet" + : "Desktop", }); - console.log('The user agent uses ' + userAgent.browser + ' on a ' + (userAgent.isMobile ? 'mobile' : (userAgent.isTablet ? 'tablet' : 'desktop')) + ' device.'); + console.log( + "The user agent uses " + + userAgent.browser + + " on a " + + (userAgent.isMobile + ? "mobile" + : userAgent.isTablet + ? "tablet" + : "desktop") + + " device." + ); - log('Click recorded', { urlId: url._id, country, browser: userAgent.browser, device: userAgent.isMobile ? 'Mobile' : (userAgent.isTablet ? 'Tablet' : 'Desktop') }); - log('Redirecting', { originalUrl: url.original_url }); + log("Click recorded", { + urlId: url._id, + country, + browser: userAgent.browser, + device: userAgent.isMobile + ? "Mobile" + : userAgent.isTablet + ? "Tablet" + : "Desktop", + }); + log("Redirecting", { originalUrl: url.original_url }); res.redirect(url.original_url); - } catch (error) { - log('Error processing request', { error: error.message, stack: error.stack }); - res.status(500).send('An error occurred while processing your request'); + log("Error processing request", { + error: error.message, + stack: error.stack, + }); + res.status(500).send("An error occurred while processing your request"); } }); -app.post('/:code/verify', async (req, res) => { +app.post("/:code/verify", async (req, res) => { const { code } = req.params; const { password } = req.body; const ip = req.ip; try { - const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }] }); + const url = await Url.findOne({ + $or: [{ short_code: code }, { custom_alias: code }], + }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } // Check the number of failed attempts in the last 5 minutes @@ -654,119 +773,131 @@ app.post('/:code/verify', async (req, res) => { const failedAttempts = await FailedAttempt.countDocuments({ url_id: url._id, ip_address: ip, - attempted_at: { $gt: fiveMinutesAgo } + attempted_at: { $gt: fiveMinutesAgo }, }); if (failedAttempts >= 5) { - return res.status(429).json({ error: 'Too many failed attempts. Please try again later.' }); + return res + .status(429) + .json({ error: "Too many failed attempts. Please try again later." }); } if (password !== url.password) { // Record failed attempt await FailedAttempt.create({ url_id: url._id, - ip_address: ip + ip_address: ip, }); - return res.status(401).json({ error: 'Incorrect password' }); + return res.status(401).json({ error: "Incorrect password" }); } // Password is correct, proceed with redirection res.json({ success: true, redirectUrl: url.original_url }); - } catch (error) { - console.error('Error verifying password:', error); - res.status(500).json({ error: 'An error occurred while verifying the password' }); + console.error("Error verifying password:", error); + res + .status(500) + .json({ error: "An error occurred while verifying the password" }); } }); -app.get('/stats/:code', async (req, res) => { +app.get("/stats/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; try { const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } const clicks = await Click.find({ url_id: url._id }); - const failedAttempts = await FailedAttempt.countDocuments({ url_id: url._id }); + const failedAttempts = await FailedAttempt.countDocuments({ + url_id: url._id, + }); res.json({ url, clicks, failedAttempts }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching click statistics' }); + res.status(500).json({ error: "Error fetching click statistics" }); } }); -app.get('/url/:code', async (req, res) => { +app.get("/url/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; try { const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } res.json(url); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching URL' }); + res.status(500).json({ error: "Error fetching URL" }); } }); -app.put('/url/:code', async (req, res) => { +app.put("/url/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; - const { maxUses, autoDeleteAt, whitelistMode, allowedCountries, blockedCountries, password } = req.body; + const { + maxUses, + autoDeleteAt, + whitelistMode, + allowedCountries, + blockedCountries, + password, + } = req.body; try { const url = await Url.findOneAndUpdate( { $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }, { max_uses: maxUses, - auto_delete_at: autoDeleteAt === '' ? null : autoDeleteAt, + auto_delete_at: autoDeleteAt === "" ? null : autoDeleteAt, whitelist_mode: whitelistMode, allowed_countries: allowedCountries, blocked_countries: blockedCountries, - password: password + password: password, }, { new: true } ); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } - res.json({ message: 'URL updated successfully' }); + res.json({ message: "URL updated successfully" }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error updating URL' }); + res.status(500).json({ error: "Error updating URL" }); } }); -app.delete('/url/:code', async (req, res) => { +app.delete("/url/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; @@ -774,86 +905,96 @@ app.delete('/url/:code', async (req, res) => { try { const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } await Click.deleteMany({ url_id: url._id }); await FailedAttempt.deleteMany({ url_id: url._id }); await Url.findByIdAndDelete(url._id); - res.json({ message: 'URL and associated data deleted successfully' }); + res.json({ message: "URL and associated data deleted successfully" }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error deleting URL' }); + res.status(500).json({ error: "Error deleting URL" }); } }); -app.get('/find/:code', async (req, res) => { +app.get("/find/:code", async (req, res) => { const { code } = req.params; try { - const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }] }); + const url = await Url.findOne({ + $or: [{ short_code: code }, { custom_alias: code }], + }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } - const fullUrl = `${req.protocol}://${req.get('host')}/${url.short_code}`; + const fullUrl = `${req.protocol}://${req.get("host")}/${url.short_code}`; res.json({ fullUrl }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error finding URL' }); + res.status(500).json({ error: "Error finding URL" }); } }); // Debug route -app.get('/debug/:code', async (req, res) => { +app.get("/debug/:code", async (req, res) => { const { code } = req.params; try { - const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }] }); + const url = await Url.findOne({ + $or: [{ short_code: code }, { custom_alias: code }], + }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } const clickCount = await Click.countDocuments({ url_id: url._id }); - const failedAttempts = await FailedAttempt.countDocuments({ url_id: url._id }); + const failedAttempts = await FailedAttempt.countDocuments({ + url_id: url._id, + }); res.json({ url, click_count: clickCount, failed_attempts: failedAttempts }); } catch (error) { console.error(error); - res.status(500).json({ error: 'Error fetching debug information' }); + res.status(500).json({ error: "Error fetching debug information" }); } }); -app.get('/qr/:code', async (req, res) => { +app.get("/qr/:code", async (req, res) => { if (!req.user) { - return res.status(401).json({ error: 'Unauthorized' }); + return res.status(401).json({ error: "Unauthorized" }); } const { code } = req.params; - + try { const url = await Url.findOne({ $or: [{ short_code: code }, { custom_alias: code }], - user_id: req.user._id + user_id: req.user._id, }); if (!url) { - return res.status(404).json({ error: 'URL not found' }); + return res.status(404).json({ error: "URL not found" }); } - const fullUrl = `${req.protocol}://${req.get('host')}/${url.short_code}`; + const fullUrl = `${req.protocol}://${req.get("host")}/${url.short_code}`; const qrCode = await QRCode.toDataURL(fullUrl); - + res.json({ qrCode }); } catch (error) { - console.error('Error generating QR code:', error); - res.status(500).json({ error: 'An error occurred while generating the QR code' }); + console.error("Error generating QR code:", error); + res + .status(500) + .json({ error: "An error occurred while generating the QR code" }); } }); // Helper functions for fetching analytics data async function getTotalClicks(userId) { - return Click.countDocuments({ url_id: { $in: await Url.find({ user_id: userId }).distinct('_id') } }); + return Click.countDocuments({ + url_id: { $in: await Url.find({ user_id: userId }).distinct("_id") }, + }); } async function getTotalUrls(userId) { @@ -862,14 +1003,18 @@ async function getTotalUrls(userId) { async function getCTROverTime(userId) { const clicks = await Click.aggregate([ - { $match: { url_id: { $in: await Url.find({ user_id: userId }).distinct('_id') } } }, + { + $match: { + url_id: { $in: await Url.find({ user_id: userId }).distinct("_id") }, + }, + }, { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$clicked_at" } }, - clicks: { $sum: 1 } - } + clicks: { $sum: 1 }, + }, }, - { $sort: { _id: 1 } } + { $sort: { _id: 1 } }, ]); const urls = await Url.aggregate([ @@ -877,17 +1022,17 @@ async function getCTROverTime(userId) { { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } }, - urls: { $sum: 1 } - } + urls: { $sum: 1 }, + }, }, - { $sort: { _id: 1 } } + { $sort: { _id: 1 } }, ]); - const ctrData = clicks.map(click => { - const urlCount = urls.find(url => url._id === click._id)?.urls || 1; + const ctrData = clicks.map((click) => { + const urlCount = urls.find((url) => url._id === click._id)?.urls || 1; return { date: click._id, - ctr: click.clicks / urlCount + ctr: click.clicks / urlCount, }; }); @@ -896,34 +1041,38 @@ async function getCTROverTime(userId) { async function getGeoDistribution(userId) { const geoData = await Click.aggregate([ - { $match: { url_id: { $in: await Url.find({ user_id: userId }).distinct('_id') } } }, + { + $match: { + url_id: { $in: await Url.find({ user_id: userId }).distinct("_id") }, + }, + }, { $group: { _id: "$country", - count: { $sum: 1 } - } - } + count: { $sum: 1 }, + }, + }, ]); - console.log('Raw geoData:', geoData); + console.log("Raw geoData:", geoData); const result = geoData.reduce((acc, item) => { - if (item._id && item._id.length === 2) { // Ensure the country code is valid + if (item._id && item._id.length === 2) { + // Ensure the country code is valid acc[item._id] = item.count; } return acc; }, {}); - console.log('Processed geoData:', result); + console.log("Processed geoData:", result); return result; } - // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); - res.status(500).send('Something broke!'); + res.status(500).send("Something broke!"); }); // Start the server @@ -932,14 +1081,14 @@ app.listen(port, () => { }); // Graceful shutdown -process.on('SIGINT', async () => { - console.log('Closing MongoDB connection...'); +process.on("SIGINT", async () => { + console.log("Closing MongoDB connection..."); try { await mongoose.connection.close(); - console.log('MongoDB connection closed.'); + console.log("MongoDB connection closed."); process.exit(0); } catch (err) { - console.error('Error closing MongoDB connection:', err); + console.error("Error closing MongoDB connection:", err); process.exit(1); } -}); \ No newline at end of file +}); diff --git a/views/faq.ejs b/views/faq.ejs new file mode 100644 index 0000000..9f8a92a --- /dev/null +++ b/views/faq.ejs @@ -0,0 +1,77 @@ +
+
+
+

Frequently Asked Questions

+ Get Started Now +
+ +
+
+
1. What is URL Slicer?
+
+ URL Slicer is a tool designed to create shorter, more manageable links + for sharing on social media, emails, and marketing campaigns. It + simplifies long URLs into concise links that are easier to share and + remember. +
+
+ +
+
+ 2. How do I create a short link using URL Slicer? +
+
+ To create a short link, follow these steps: Sign Up: Create your free + account in seconds.
+ Paste Your URL: Enter the long URL you want to shorten.
+ Customize: Add any custom options or restrictions if needed.
+ Share: Copy your new short URL and share it wherever you like.. +
+
+ +
+
3. Is URL Slicer free to use?
+
+ Yes, URL Slicer is completely free to use. You can create an account, + shorten URLs, and access analytics without any cost. +
+
+ +
+
+ 4. Can I track the performance of my links? +
+
+ Yes, URL Slicer provides detailed analytics to track the performance + of your links. You can monitor clicks, traffic sources, and other + metrics to measure the effectiveness of your campaigns. +
+
+ +
+
+ 5. How secure are the links created with URL Slicer? +
+
+ URL Slicer uses advanced security features to protect your links. You + can set expiration dates, usage limits, and geo-restrictions to ensure + your links are safe and secure. +
+
+
+
+
+ + diff --git a/views/index.ejs b/views/index.ejs index 94319e6..213271b 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -3,16 +3,28 @@

Simplify Your Links, Amplify Your Reach

-

URL Slicer is your go-to tool for creating shorter, more manageable links. Whether you're sharing on social media, sending emails, or managing marketing campaigns, our powerful URL shortener has got you covered.

- +

+ URL Slicer is your go-to tool for creating shorter, more manageable links. + Whether you're sharing on social media, sending emails, or managing + marketing campaigns, our powerful URL shortener has got you covered. +

+ <% if (typeof user !== 'undefined' && user) { %> -

Hello, <%= user.email %>!

- Go to Dashboard +

+ Hello, <%= user.email %>! +

+ Go to Dashboard <% } else { %> +
+
+ FAQ +
+
+ <% } %>
@@ -23,22 +35,31 @@

Custom Short Links

-

Create memorable, branded short links that reflect your identity.

+

+ Create memorable, branded short links that reflect your identity. +

Detailed Analytics

-

Track clicks, analyze traffic sources, and measure your link performance.

+

+ Track clicks, analyze traffic sources, and measure your link + performance. +

Advanced Security

-

Set expiration dates, usage limits, and geo-restrictions for your links.

+

+ Set expiration dates, usage limits, and geo-restrictions for your links. +

Mobile Friendly

-

Manage your links on-the-go with our responsive design.

+

+ Manage your links on-the-go with our responsive design. +

@@ -59,7 +80,9 @@
3

Customize

-

Add custom options and restrictions if needed.

+

+ Add custom options and restrictions if needed. +

4
@@ -71,10 +94,12 @@

Ready to Slice Your URLs?

-

Join us today and start creating powerful short links that drive results.

+

+ Join us today and start creating powerful short links that drive results. +

Get Started Now
\ No newline at end of file +