├── .github ├── PULL_REQUEST_TEMPLATE.md ├── copilot-instructions.md └── workflows │ └── autoFormatCode.yml ├── .gitignore ├── .idea └── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LEARN.md ├── LICENSE ├── README.md ├── backend ├── .gitignore ├── .prettierrc ├── configs │ ├── EmailConfig.js │ └── dbConfig.js ├── controllers │ ├── adminControllers │ │ ├── adminForgotPassword.js │ │ ├── adminLogin.js │ │ ├── adminResetPassword.js │ │ ├── adminSignup.js │ │ ├── adminVerifyEmail.js │ │ └── manageReviews.js │ ├── authControllers │ │ ├── changeEmail.js │ │ ├── changeName.js │ │ ├── forgotPassword.js │ │ ├── getCurrentUser.js │ │ ├── login.js │ │ ├── resetPassword.js │ │ ├── signup.js │ │ └── verifyEmail.js │ ├── domainControllers │ │ └── addCustomDomain.js │ ├── feedbackControllers │ │ ├── feedback.js │ │ └── reviews.js │ └── urlControllers │ │ ├── category.js │ │ ├── deleteUrl.js │ │ ├── exportGeneratedUrls.js │ │ ├── generateCustomBackHalf.js │ │ ├── generateShortUrl.js │ │ ├── getHistory.js │ │ └── redirectToURL.js ├── extras │ ├── Constants.js │ └── passport.js ├── index.js ├── jobs │ └── customDomainJobs.js ├── middlewares │ ├── ValidatorErrorHandler.js │ ├── authMiddleware.js │ └── isAdmin.js ├── models │ ├── AdminModel.js │ ├── CustomDomainsModel.js │ ├── Feedback.js │ ├── LinkInBioPageModel.js │ ├── Tokenmodel.js │ ├── UrlModel.js │ └── UserModel.js ├── nodemon.json ├── package.json ├── routes │ ├── admin.route.js │ ├── adminAuth.route.js │ ├── domain.route.js │ ├── url.route.js │ └── userAuth.route.js ├── swagger.yml ├── test │ ├── mainTest.js │ └── urlController.test.js ├── utils │ ├── dns.js │ ├── helperfunc.js │ └── mailSend.js ├── validators │ ├── authValidators.js │ ├── feedbackValidator.js │ └── urlValidator.js └── views │ ├── reset_password_email_template.ejs │ └── welcome_email_template.ejs ├── chrome-extension ├── extension.html ├── history.html ├── history.js ├── icon.png ├── logo.png ├── manifest.json └── script.js ├── designs ├── User-Profile-Page │ ├── Desktop - User Page - Favorites.png │ ├── Desktop - User Page - Links.png │ ├── Desktop - User Page - Profile edit Settings.png │ ├── Desktop - User Page - Profile edit Settings_2.png │ ├── FigmaLink.txt │ ├── Mobile - User Page - Favourites.png │ ├── Mobile - User Page - Links.png │ └── Mobile - User Page - Profile Edit.png ├── profile-finder-page │ ├── desktop │ │ ├── desktop 1 (no description).pdf │ │ ├── desktop 1 (no description).png │ │ ├── desktop 1.pdf │ │ ├── desktop 1.png │ │ ├── desktop 2 (no description).pdf │ │ ├── desktop 2 (no description).png │ │ ├── desktop 2.pdf │ │ └── desktop 2.png │ └── mobile │ │ ├── mobile 1 (no description).pdf │ │ ├── mobile 1 (no description).png │ │ ├── mobile 1.pdf │ │ ├── mobile 1.png │ │ ├── mobile 2 (no description).pdf │ │ ├── mobile 2 (no description).png │ │ ├── mobile 2.pdf │ │ └── mobile 2.png └── public-profile-page │ ├── desktop 1.pdf │ ├── desktop │ ├── desktop 1.pdf │ └── desktop2.pdf │ ├── desktop2.pdf │ ├── mobile │ ├── mobile1.pdf │ ├── mobile1.png │ ├── mobile2.pdf │ └── mobile2.png │ ├── mobile1.pdf │ ├── mobile1.png │ ├── mobile2.pdf │ └── mobile2.png ├── frontend ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── index.html ├── package.json ├── public │ ├── _redirects │ ├── favicon.ico │ ├── logo.png │ └── manifest.json ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ ├── cont1.json │ │ └── images │ │ │ └── feedback.png │ ├── components │ │ ├── About.jsx │ │ ├── AxiosFetch.jsx │ │ ├── Contributers.css │ │ ├── Contributers.jsx │ │ ├── Contributors.css │ │ ├── Contributors.jsx │ │ ├── ExportToExcel.jsx │ │ ├── FeedbackForm.css │ │ ├── FeedbackForm.jsx │ │ ├── Footer.css │ │ ├── Footer.jsx │ │ ├── ForgotPassword.jsx │ │ ├── History.jsx │ │ ├── HistoryCard.jsx │ │ ├── Home.jsx │ │ ├── LandingPage │ │ │ ├── Hero.jsx │ │ │ ├── HomePage.jsx │ │ │ ├── Images │ │ │ │ ├── ShortenDesktop.svg │ │ │ │ ├── ShortenMobile.svg │ │ │ │ ├── desktop.svg │ │ │ │ ├── facebook.svg │ │ │ │ ├── img.svg │ │ │ │ ├── instagram.svg │ │ │ │ ├── mobile.svg │ │ │ │ ├── pinterest.svg │ │ │ │ ├── twitter.svg │ │ │ │ └── working_img.svg │ │ │ ├── LandingPage.css │ │ │ ├── LandingPage.jsx │ │ │ ├── appTheme.js │ │ │ ├── footer_segment │ │ │ │ ├── BoostBox.jsx │ │ │ │ ├── Footer.jsx │ │ │ │ ├── FooterLink.jsx │ │ │ │ ├── FooterLinkList.jsx │ │ │ │ └── FooterSegment.jsx │ │ │ └── form │ │ │ │ ├── BackHalfForm.jsx │ │ │ │ ├── LinkForm.jsx │ │ │ │ ├── LinkList.jsx │ │ │ │ ├── LinkParent.jsx │ │ │ │ ├── Schema.jsx │ │ │ │ └── SingleOutput.jsx │ │ ├── Linkinbio.jsx │ │ ├── Login.jsx │ │ ├── Logout.jsx │ │ ├── Navbar.jsx │ │ ├── Pagination.jsx │ │ ├── ProfileForm.jsx │ │ ├── ProfilePage.jsx │ │ ├── ResetPassword.jsx │ │ ├── Sharepage.css │ │ ├── Sharepage.jsx │ │ ├── Signup.jsx │ │ └── linkinbio.css │ ├── context │ │ └── UserContext.js │ └── index.jsx └── vite.config.js ├── netlify.toml └── package.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | 3 | 4 | Fixes # (issue) 5 | 6 | ## Requirement/Documentation 7 | 8 | 9 | 10 | - If there is a requirement document, please, share it here. 11 | - If there is ab UI/UX design document, please, share it here. 12 | 13 | ## Type of change 14 | 15 | 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] Chore (refactoring code, workflow improvements) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | ## How should this be tested? 24 | 25 | 26 | 27 | - Are there environment variables that should be set? 28 | - What are the minimal test data to have? 29 | - Any other important info that could help to test that PR 30 | 31 | ## Checklist 32 | 33 | 34 | 35 | - I haven't read the [contributing guide](https://github.com/DhananjayThomble/URL-Shortener-App/blob/main/CONTRIBUTING.md) 36 | - My code doesn't follow the style guidelines of this project 37 | - I haven't commented my code, particularly in hard-to-understand areas 38 | - I haven't checked if my PR needs changes to the documentation 39 | - I haven't checked if my changes generate no new warnings 40 | -------------------------------------------------------------------------------- /.github/workflows/autoFormatCode.yml: -------------------------------------------------------------------------------- 1 | name: Auto Format Code with Prettier 2 | # This action automatically formats code with Prettier when a push is made to the main branch. 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "14.x" 20 | 21 | - name: Install dependencies 22 | run: npm install --prefix backend && npm install --prefix frontend 23 | 24 | - name: Run Prettier in /backend 25 | run: npm run format --prefix backend 26 | 27 | - name: Run Prettier in /frontend 28 | run: npm run format --prefix frontend 29 | 30 | - name: Check for changes 31 | id: check_changes 32 | run: | 33 | git diff --exit-code --quiet || echo "::set-output name=changes::true" 34 | 35 | - name: Configure Git 36 | run: | 37 | git config user.name "github-actions[bot]" 38 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 39 | 40 | - name: Commit files 41 | if: steps.check_changes.outputs.changes == 'true' 42 | run: | 43 | git add . 44 | git commit -m "Auto formatting with Prettier" 45 | git push 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | .env 3 | node_modules/ 4 | resources/ 5 | extras/todo 6 | .idea 7 | # Local Netlify folder 8 | .netlify 9 | package-lock.json 10 | yarn.lock 11 | npm-debug.log 12 | yarn-error.log 13 | build/ 14 | dist/ 15 | .db 16 | .vscode/ 17 | .idea/ 18 | .DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to URL-Shortener-App 2 | 3 | Thank you for your interest in contributing to URL-Shortener-App! We welcome contributions from the community to make this project better. Before you get started, please take a moment to review the following guidelines. 4 | 5 | ## Table of Contents 6 | - [Code of Conduct](#code-of-conduct) 7 | - [How to Contribute](#how-to-contribute) 8 | - [Reporting Bugs](#reporting-bugs) 9 | - [Adding Features](#adding-features) 10 | - [Code Style](#code-style) 11 | - [Documentation](#documentation) 12 | - [Pull Requests](#pull-requests) 13 | - [Getting Started](#getting-started) 14 | - [License](#license) 15 | 16 | ## Code of Conduct 17 | 18 | Please review our [Code of Conduct](CODE_OF_CONDUCT.md) before participating in our community. 19 | 20 | ## How to Contribute 21 | 22 | ### Reporting Bugs 23 | 24 | If you encounter a bug while using URL-Shortener-App, please [open an issue](../../issues) on our GitHub repository. Make sure to include: 25 | 26 | - A clear and descriptive title. 27 | - A detailed description of the issue, including steps to reproduce it. 28 | - Information about your environment, such as operating system and browser (if applicable). 29 | 30 | ### Adding Features 31 | 32 | If you have an idea for a new feature or improvement, feel free to suggest it by [opening an issue](../../issues). We appreciate well-documented feature proposals that include: 33 | 34 | - A clear and descriptive title. 35 | - A detailed explanation of the feature. 36 | - Any relevant design or implementation details. 37 | 38 | ### Code Style 39 | 40 | Please adhere to our [code style guidelines](CODE_STYLE.md) when contributing code to this project. 41 | 42 | ### Documentation 43 | 44 | Improvements to documentation are always welcome. If you find areas where the documentation can be enhanced, or if you want to contribute new documentation, please do so. 45 | 46 | ### Pull Requests 47 | 48 | When submitting pull requests, please follow these guidelines: 49 | 50 | 1. Fork the repository and create a feature branch from the `main` branch. 51 | 2. Ensure your code adheres to the code style guidelines. 52 | 3. Write clear, concise commit messages. 53 | 4. Test your changes thoroughly. 54 | 5. Ensure that your pull request addresses an open issue or clearly states its purpose. 55 | 6. Reference any related issues in your pull request description. 56 | 57 | ## Getting Started 58 | 59 | If you're new to contributing to open-source projects, you can start by checking out the list of open issues labeled as "good first issue." These issues are typically beginner-friendly. 60 | 61 | ## License 62 | 63 | By contributing to [URL-Shorter-App](https://app.snapurl.in/) , you agree that your contributions will be licensed under the project's [LICENSE](LICENSE). 64 | 65 | Thank you for your contributions! 🚀 66 | -------------------------------------------------------------------------------- /LEARN.md: -------------------------------------------------------------------------------- 1 | # Building a URL Shortener with Node.js, React, and MongoDB 2 | 3 | This project demonstrates how to build a URL shortener using Node.js, React, and MongoDB. The application allows users to create shortened URLs that redirect to the original URL. It also tracks the number of visits to each shortened URL. 4 | 5 | ## Project Structure 6 | 7 | The project consists of a backend API and a frontend web application. 8 | 9 | ### Backend 10 | 11 | The backend is built using Node.js and Express.js. It provides the following functionalities: 12 | 13 | - User authentication and authorization using JSON Web Tokens (JWT) 14 | - URL shortening using a random string of characters 15 | - Visit count tracking for shortened URLs 16 | - CRUD operations for URLs 17 | - Exporting URL history to an Excel sheet 18 | 19 | The backend API is secured using CORS and rate limiting is implemented using Express Rate Limit. 20 | 21 | ### Frontend 22 | 23 | The frontend is built using React and Bootstrap. It provides the following functionalities: 24 | 25 | - User registration and login 26 | - URL shortening form 27 | - Displaying all shortened URLs generated by a user 28 | - Exporting URL history to an Excel sheet 29 | - Deleting shortened URLs 30 | - Navigation using react-router-dom 31 | - Integration with the backend API with Axios 32 | 33 | ## GitHub Actions: 34 | 35 | [//]: # (formating code whenever a push is made to the master branch) 36 | 37 | ```yml 38 | name: Auto Format Code with Prettier 39 | # This action automatically formats code with Prettier when a push is made to the main branch. 40 | 41 | on: 42 | push: 43 | branches: [main] 44 | 45 | jobs: 46 | format: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-node@v1 51 | with: 52 | node-version: "14.x" 53 | - run: npm install 54 | - run: npm run format 55 | 56 | # if there are changes, commit and push them 57 | - name: Commit files 58 | run: | 59 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 60 | git config --local user.name "github-actions[bot]" 61 | git add . 62 | git commit -m "Auto formatting with Prettier" 63 | git push 64 | 65 | ``` 66 | 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dhananjay Thomble 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /backend/configs/EmailConfig.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | const nodemailer = require('nodemailer'); 5 | 6 | let transporter = nodemailer.createTransport({ 7 | host: process.env.EMAIL_HOST, 8 | port: process.env.EMAIL_PORT, 9 | secure: false, 10 | auth: { 11 | user: process.env.EMAIL_USER, 12 | pass: process.env.EMAIL_PASS, 13 | }, 14 | }); 15 | 16 | module.exports = transporter; 17 | -------------------------------------------------------------------------------- /backend/configs/dbConfig.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { configDotenv } from 'dotenv'; 3 | configDotenv(); 4 | 5 | mongoose.set('strictQuery', false); 6 | mongoose.set('debug', process.env.NODE_ENV === 'development'); // if dev mode, log all mongoose query 7 | 8 | async function mongoConnect() { 9 | try { 10 | await mongoose.connect(process.env.DB_URL); 11 | console.log('Connected to DB'); 12 | } catch (err) { 13 | console.error(err); 14 | } 15 | } 16 | 17 | export default mongoConnect; 18 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/adminForgotPassword.js: -------------------------------------------------------------------------------- 1 | import Admin from '../../models/AdminModel.js'; 2 | import crypto from 'crypto'; 3 | import TokenModel from '../../models/Tokenmodel.js'; 4 | import { sendEmail } from '../../utils/mailSend.js'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | const generatePasswordResetToken = async (email) => { 9 | // get user from database 10 | const user = await Admin.findOne({ email }); 11 | 12 | if (!user) { 13 | throw new Error('User not found'); 14 | } 15 | 16 | // generate password reset token 17 | const token = crypto.randomBytes(32).toString('hex'); 18 | const expiration = Date.now() + 3600000; 19 | 20 | const resetToken = new TokenModel({ 21 | userId: user._id, 22 | token, 23 | expiration, 24 | }); 25 | 26 | // save password reset token in database 27 | await resetToken.save(); 28 | 29 | // send password reset token 30 | const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; 31 | const emailOptions = { 32 | from: 'SnapURL@dturl.live', 33 | subject: 'Password Reset for SnapURL', 34 | recipient: email, 35 | html: `Click here to reset your password.`, 36 | }; 37 | 38 | await sendEmail(emailOptions); 39 | }; 40 | 41 | // Forgot Password controller 42 | export const adminForgotPassword = async (req, res) => { 43 | try { 44 | const { email } = req.body; 45 | // check that email existis in database or not 46 | const result = await Admin.findOne({ email }); 47 | if (!result) { 48 | return res.status(404).json({ message: 'Email not found' }); 49 | } 50 | 51 | await generatePasswordResetToken(email); 52 | return res.status(200).json({ message: 'Password reset email sent' }); 53 | } catch (error) { 54 | console.error(error); 55 | res.status(500).json({ error: 'Server error' }); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/adminLogin.js: -------------------------------------------------------------------------------- 1 | import Admin from '../../models/AdminModel.js'; 2 | import jwt from 'jsonwebtoken'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const adminLogin = async (req, res) => { 7 | try { 8 | // get email and password from request body 9 | const { email, password } = req.body; 10 | 11 | const user = await Admin.findOne({ email }); 12 | 13 | // if user not found, 401 status for unauthorized 14 | if (!user) return res.status(401).json({ error: 'Email does not exist' }); 15 | 16 | // check password using passport 17 | user.comparePassword(password, (err, isMatch) => { 18 | if (!isMatch) 19 | return res.status(401).json({ error: 'Invalid credentials' }); 20 | 21 | // password matched, create a token 22 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, { 23 | expiresIn: '7d', 24 | }); 25 | 26 | // send token in response 27 | const { _id, name, email } = user; 28 | return res.status(200).json({ token, user: { _id, name, email } }); 29 | }); 30 | } catch (error) { 31 | console.error(error); 32 | res.status(500).json({ error: 'Server error' }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/adminResetPassword.js: -------------------------------------------------------------------------------- 1 | import Admin from '../../models/AdminModel.js'; 2 | import TokenModel from '../../models/Tokenmodel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const adminResetPassword = async (req, res) => { 7 | try { 8 | const { token, password } = req.body; 9 | 10 | // get reset password token from database 11 | const resetToken = await TokenModel.findOne({ token }); 12 | 13 | // verify expiration date of reset password token 14 | if (!resetToken || resetToken.expiration < Date.now()) { 15 | return res.status(400).json({ error: 'Invalid or expired token' }); 16 | } 17 | 18 | // update user Password if token is valid 19 | const user = await Admin.findById(resetToken.userId); 20 | user.password = password; 21 | await user.save(); 22 | 23 | // remove password reset token from database 24 | await resetToken.remove(); 25 | 26 | return res.status(200).json({ message: 'Password reset successful' }); 27 | } catch (error) { 28 | console.error(error); 29 | res.status(500).json({ error: 'Server error' }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/adminSignup.js: -------------------------------------------------------------------------------- 1 | import Admin from '../../models/AdminModel.js'; 2 | import { sendWelcomeEmail } from '../../utils/mailSend.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const adminSignup = async (req, res) => { 7 | try { 8 | // get name and pasword fields from request body 9 | const { name, password } = req.body; 10 | 11 | // Validate that name is at least three characters long 12 | if (!name || name.length < 3) 13 | return res.status(400).json({ 14 | error: 'Name is required and should be at least 3 characters long', 15 | }); 16 | 17 | // Validate that password is at least six characters long 18 | if (!password || password.length < 6) 19 | return res.status(400).json({ 20 | error: 'Password is required and should be at least 6 characters long', 21 | }); 22 | 23 | // register the user in the database 24 | const user = new Admin(req.body); 25 | 26 | await user.save(); 27 | console.log('User saved'); 28 | 29 | // Sending the welcome email 30 | await sendWelcomeEmail(user.name, user.email); 31 | 32 | return res.status(200).json({ ok: true }); 33 | } catch (err) { 34 | console.log('CREATE USER FAILED', err); 35 | return res 36 | .status(500) 37 | .json({ error: 'Error saving user in database. Try later' }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/adminVerifyEmail.js: -------------------------------------------------------------------------------- 1 | import Admin from '../../models/AdminModel.js'; 2 | import TokenModel from '../../models/Tokenmodel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const adminVerifyEmail = async (req, res) => { 7 | try { 8 | // token validation 9 | query('token').notEmpty().trim().escape().withMessage('Token is required'); 10 | // get token from request 11 | const { token } = req.query; 12 | 13 | const verificationToken = await TokenModel.findOne({ token }); 14 | 15 | // verify token 16 | if (!verificationToken) { 17 | return res.status(400).json({ error: 'Invalid or expired token' }); 18 | } 19 | 20 | // get user by token id 21 | const user = await Admin.findById(verificationToken.userId); 22 | 23 | if (!user) { 24 | return res.status(400).json({ error: 'User not found' }); 25 | } 26 | 27 | if (user.isEmailVerified) { 28 | return res.status(400).json({ error: 'Email already verified' }); 29 | } 30 | 31 | user.isEmailVerified = true; 32 | 33 | //save user 34 | await user.save(); 35 | 36 | await verificationToken.remove(); 37 | 38 | console.log('Email verification done'); 39 | 40 | return res.status(200).json({ message: 'Email verification successful' }); 41 | } catch (error) { 42 | console.error('Error verifying email:', error); 43 | res.status(500).json({ error: 'Server error' }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /backend/controllers/adminControllers/manageReviews.js: -------------------------------------------------------------------------------- 1 | import Feedback from '../../models/Feedback.js'; 2 | 3 | export const deleteReview = async (req, res) => { 4 | const reviewId = req.params; 5 | 6 | try { 7 | const deletedReview = await Feedback.deleteOne(reviewId); 8 | 9 | if (!deletedReview) { 10 | return res.status(404).json({ error: 'Review not found' }); 11 | } 12 | 13 | return res.status(200).json({ message: 'Review deleted successfully' }); 14 | } catch (error) { 15 | console.error(error); 16 | return res.status(500).json({ error: 'Server error' }); 17 | } 18 | }; 19 | 20 | export const deleteAllFeedback = async (req, res) => { 21 | try { 22 | const result = await Feedback.deleteMany({}); 23 | if (result) { 24 | res.status(200).json({ message: 'All feedback deleted successfully' }); 25 | } 26 | console.log(`Deleted ${result.deletedCount} documents`); 27 | } catch (error) { 28 | console.error(error); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/changeEmail.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from '../../models/UserModel.js'; 3 | import dotenv from 'dotenv'; 4 | import { isValidEmail } from '../../utils/helperfunc.js'; 5 | dotenv.config(); 6 | 7 | export const changeEmail = async (req, res) => { 8 | try { 9 | const token = req.headers.authorization?.split(' ')[1]; 10 | 11 | jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => { 12 | if (err) { 13 | return res.status(401).json({ message: 'Unauthorized' }); 14 | } 15 | 16 | const userId = decoded._id; 17 | 18 | try { 19 | // Retrieve the user from the database using the user ID 20 | const user = await User.findById(userId); 21 | 22 | if (!user) { 23 | return res.status(401).json({ message: 'Unauthorized' }); 24 | } 25 | 26 | // Update the user's email 27 | user.email = newEmail; 28 | 29 | // Save the updated user 30 | await user.save(); 31 | 32 | return res.status(200).json({ message: 'Email updated successfully' }); 33 | } catch (err) { 34 | console.log(err); 35 | return res.status(500).json({ error: 'Failed to update email' }); 36 | } 37 | }); 38 | } catch (err) { 39 | console.log(err); 40 | res.status(500).json({ error: 'Server error' }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/changeName.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from '../../models/UserModel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const changeName = async (req, res) => { 7 | try { 8 | const token = req.headers.authorization?.split(' ')[1]; 9 | jwt.verify(token, process.env.JWT_SECRET, async (err, decoded) => { 10 | if (err) { 11 | return res.status(401).json({ message: 'Unauthorized' }); 12 | } 13 | 14 | const userId = decoded._id; 15 | 16 | try { 17 | // Retrieve the user from the database using the user ID 18 | const user = await User.findById(userId); 19 | 20 | if (!user) { 21 | return res.status(401).json({ message: 'Unauthorized' }); 22 | } 23 | 24 | const newName = req.body.name; 25 | 26 | // Update the user's name 27 | user.name = newName; 28 | 29 | // Save the updated user 30 | await user.save(); 31 | 32 | return res.status(200).json({ message: 'Name updated successfully' }); 33 | } catch (err) { 34 | console.log(err); 35 | return res.status(500).json({ error: 'Failed to update Name' }); 36 | } 37 | }); 38 | } catch (err) { 39 | console.log(err); 40 | res.status(500).json({ error: 'Server error' }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/forgotPassword.js: -------------------------------------------------------------------------------- 1 | import User from '../../models/UserModel.js'; 2 | import crypto from 'crypto'; 3 | import TokenModel from '../../models/Tokenmodel.js'; 4 | import { sendPasswordResetEmail } from '../../utils/mailSend.js'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | const generatePasswordResetToken = async (userId) => { 9 | // generate password reset token 10 | const token = crypto.randomBytes(32).toString('hex'); 11 | const expiration = Date.now() + 3600000; // 1 hour from now 12 | 13 | const resetToken = new TokenModel({ 14 | userId, 15 | token, 16 | expiration, 17 | }); 18 | 19 | // save password reset token in database 20 | await resetToken.save(); 21 | 22 | return token; 23 | }; 24 | 25 | // Forgot Password controller 26 | export const forgotPassword = async (req, res) => { 27 | try { 28 | const { email } = req.body; 29 | // check that email existis in database or not 30 | const user = await User.findOne({ email }); 31 | if (!user) { 32 | return res.status(404).json({ message: 'Email not found' }); 33 | } 34 | 35 | const token = await generatePasswordResetToken(user._id); 36 | 37 | // send password reset token 38 | const resetLink = `${process.env.FRONTEND_URL}/reset-password?token=${token}`; 39 | 40 | await sendPasswordResetEmail(email, resetLink); 41 | 42 | return res.status(200).json({ message: 'Password reset email sent' }); 43 | } catch (error) { 44 | console.error(error); 45 | res.status(500).json({ error: 'Server error' }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/getCurrentUser.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import User from '../../models/UserModel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const getCurrentUser = (req, res) => { 7 | // Check if a token is present in the request headers 8 | // split the token from the "Bearer" string 9 | const token = req.headers.authorization?.split(' ')[1]; 10 | 11 | if (!token) { 12 | return res.status(401).json({ message: 'Unauthorized' }); 13 | } 14 | 15 | // Verify and decode the token 16 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { 17 | if (err) { 18 | return res.status(401).json({ message: 'Unauthorized' }); 19 | } 20 | 21 | // The decoded object contains the user's information 22 | const userId = decoded._id; 23 | 24 | // Retrieve the user from the database using the user ID 25 | User.findById(userId, (err, user) => { 26 | if (err || !user) { 27 | return res.status(401).json({ message: 'Unauthorized' }); 28 | } 29 | 30 | // Send the user's information in the response 31 | return res.status(200).json({ user }); 32 | }); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/login.js: -------------------------------------------------------------------------------- 1 | import User from '../../models/UserModel.js'; 2 | import jwt from 'jsonwebtoken'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const login = async (req, res) => { 7 | try { 8 | // get email and password from request body 9 | const { email, password } = req.body; 10 | 11 | const user = await User.findOne({ email }); 12 | 13 | // if user not found, 401 status for unauthorized 14 | if (!user) return res.status(401).json({ error: 'Email does not exist' }); 15 | 16 | // check password using passport 17 | user.comparePassword(password, (err, isMatch) => { 18 | if (!isMatch) 19 | return res.status(401).json({ error: 'Invalid credentials' }); 20 | 21 | // password matched, create a token 22 | const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, { 23 | expiresIn: '7d', 24 | }); 25 | 26 | // send token in response 27 | const { _id, name, email } = user; 28 | return res.status(200).json({ token, user: { _id, name, email } }); 29 | }); 30 | } catch (error) { 31 | console.error(error); 32 | res.status(500).json({ error: 'Server error' }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/resetPassword.js: -------------------------------------------------------------------------------- 1 | import User from '../../models/UserModel.js'; 2 | import TokenModel from '../../models/Tokenmodel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const resetPassword = async (req, res) => { 7 | try { 8 | const { token, password } = req.body; 9 | 10 | // get reset password token from database 11 | const resetToken = await TokenModel.findOne({ token }); 12 | 13 | // verify expiration date of reset password token 14 | if (!resetToken || resetToken.expiration < Date.now()) { 15 | return res.status(400).json({ error: 'Invalid or expired token' }); 16 | } 17 | 18 | // update user Password if token is valid 19 | const user = await User.findById(resetToken.userId); 20 | user.password = password; 21 | await user.save(); 22 | 23 | // remove password reset token from database 24 | await resetToken.remove(); 25 | 26 | return res.status(200).json({ message: 'Password reset successful' }); 27 | } catch (error) { 28 | console.error(error); 29 | res.status(500).json({ error: 'Server error' }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/signup.js: -------------------------------------------------------------------------------- 1 | import User from '../../models/UserModel.js'; 2 | import { sendWelcomeEmail } from '../../utils/mailSend.js'; 3 | import dotenv from 'dotenv'; 4 | 5 | dotenv.config(); 6 | 7 | export const signup = async (req, res) => { 8 | try { 9 | // all the fields are validated by the middleware in the route 10 | const { name, email, password } = req.body; 11 | // register the user in the database 12 | const user = new User({ name, email, password }); 13 | 14 | await user.save(); 15 | 16 | // send welcome email to the user 17 | await sendWelcomeEmail(name, email, user._id); 18 | 19 | res.status(200).json({ ok: true }); 20 | } catch (err) { 21 | console.error(err); 22 | res.status(500).json({ error: 'Error saving user in database. Try later' }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /backend/controllers/authControllers/verifyEmail.js: -------------------------------------------------------------------------------- 1 | import User from '../../models/UserModel.js'; 2 | import TokenModel from '../../models/Tokenmodel.js'; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | export const verifyEmail = async (req, res) => { 7 | try { 8 | // get token from request 9 | const { token } = req.query; 10 | 11 | // token is already validated by the middleware 12 | 13 | const verificationToken = await TokenModel.findOne({ token }); 14 | 15 | // verify token 16 | if (!verificationToken) { 17 | return res.status(400).json({ error: 'Invalid or expired token' }); 18 | } 19 | 20 | // get user by token id 21 | const user = await User.findById(verificationToken.userId); 22 | 23 | if (!user) { 24 | return res.status(400).json({ error: 'User not found' }); 25 | } 26 | 27 | if (user.isEmailVerified) { 28 | return res.status(400).json({ error: 'Email already verified' }); 29 | } 30 | 31 | user.isEmailVerified = true; 32 | 33 | //save user 34 | await user.save(); 35 | 36 | await verificationToken.remove(); 37 | 38 | console.log('Email verification done'); 39 | 40 | return res.redirect('https://app.snapurl.in/login'); 41 | } catch (error) { 42 | console.error('Error verifying email:', error); 43 | res.status(500).json({ error: 'Server error' }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /backend/controllers/domainControllers/addCustomDomain.js: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import CustomDomain from '../../models/CustomDomainsModel.js'; 3 | import User from '../../models/UserModel.js'; 4 | 5 | export const addCustomDomain = async (req, res) => { 6 | try { 7 | const user = await User.findById(req.user._id); 8 | 9 | if (!user) { 10 | res.send('User does not exist'); 11 | return; 12 | } 13 | 14 | const { domain } = req.body; 15 | 16 | if (!domain) { 17 | res.send('Please provide a domain'); 18 | return; 19 | } 20 | 21 | const dnsVerificationCode = randomBytes(32).toString('hex'); 22 | 23 | const customDomain = await CustomDomain.create({ 24 | url: domain, 25 | dnsVerificationCode, 26 | user: req.user, 27 | }); 28 | 29 | // TODO: send email for verification process for the DNS 30 | user.customDomain = customDomain; 31 | await user.save(); 32 | 33 | res.status(200).json({ 34 | message: 35 | 'Successfully added domain! Please refer to your email to verify it.', 36 | }); 37 | } catch (error) { 38 | console.error(error); 39 | res.status(500).json({ error: 'Server error' }); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /backend/controllers/feedbackControllers/feedback.js: -------------------------------------------------------------------------------- 1 | // Route for submitting feedback 2 | import { validationResult } from 'express-validator'; 3 | import Feedback from '../../models/Feedback.js'; 4 | import User from '../../models/UserModel.js'; 5 | export const submitFeedback = async (req, res) => { 6 | try { 7 | // Implement validation using express-validator 8 | const errors = validationResult(req); 9 | if (!errors.isEmpty()) { 10 | return res.status(400).json({ errors: errors.array() }); 11 | } 12 | //search for userid which is refering to user model to extract email and name from user model 13 | const user = await User.findById(req.user._id); 14 | 15 | if (!user) { 16 | return res.status(404).json({ error: 'User not found' }); 17 | } 18 | // Destructure name and email from user 19 | const { name, email } = user; 20 | // Destructure data from the request body 21 | 22 | const { message, rating } = req.body; 23 | 24 | // Create a new feedback instance with email, username, message, and rating 25 | const feedback = new Feedback({ name, email, message, rating }); 26 | await feedback.save(); 27 | 28 | return res.status(200).send('Feedback submitted successfully.'); 29 | } catch (error) { 30 | console.error(error); 31 | return res.status(500).send('Feedback submission failed.'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /backend/controllers/feedbackControllers/reviews.js: -------------------------------------------------------------------------------- 1 | import Feedback from '../../models/Feedback.js'; 2 | //get all reviews 3 | export const getReviews = async (req, res) => { 4 | try { 5 | const reviews = await Feedback.find({}); 6 | console.log(reviews); 7 | res.status(200).json(reviews); 8 | } catch (error) { 9 | console.error(error); 10 | res.status(500).json({ error: 'Server error' }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/category.js: -------------------------------------------------------------------------------- 1 | import UrlModel from '../../models/UrlModel.js'; 2 | 3 | export const getFilteredCategory = async (req, res) => { 4 | const userId = req.user._id; 5 | const category = req.params.category; 6 | try { 7 | const urls = await UrlModel.find({ 8 | userId, 9 | category, 10 | }); 11 | // console.log(urls); 12 | res.status(200).json(urls); 13 | } catch (error) { 14 | console.error(error); 15 | res.status(500).json({ error: 'Server error' }); 16 | } 17 | }; 18 | 19 | export const updateCategory = async (req, res) => { 20 | const userId = req.user._id; 21 | const category = req.body.category; 22 | const shortUrl = req.body.shortUrl; 23 | try { 24 | const updatedUrl = await UrlModel.findOneAndUpdate( 25 | { userId, shortUrl }, // query 26 | { category }, // update 27 | ); 28 | 29 | if (!updatedUrl) { 30 | return res.status(404).json({ error: 'URL not found' }); 31 | } 32 | 33 | res.status(200).json({ message: 'Category updated' }); 34 | } catch (error) { 35 | console.error(error); 36 | res.status(500).json({ error: 'Server error' }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/deleteUrl.js: -------------------------------------------------------------------------------- 1 | import UrlModel from '../../models/UrlModel.js'; 2 | 3 | // Delete a URL associated with a user 4 | export const deleteUrl = async (req, res) => { 5 | const id = req.params.id; 6 | const userId = req.user._id; 7 | try { 8 | // Delete the URL from the database 9 | const deletedUrl = await UrlModel.findOneAndDelete({ 10 | userId, 11 | shortUrl: id, 12 | }); 13 | 14 | // if the URL does not exist 15 | if (!deletedUrl) { 16 | return res.status(400).json({ 17 | error: 'URL does not exist', 18 | }); 19 | } 20 | 21 | // send response with the success message 22 | res.status(200).json({ 23 | message: 'URL deleted', 24 | }); 25 | } catch (error) { 26 | console.error(error); 27 | res.status(500).json({ error: 'Server error' }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/exportGeneratedUrls.js: -------------------------------------------------------------------------------- 1 | import ExcelJS from 'exceljs'; 2 | import UrlModel from '../../models/UrlModel.js'; 3 | 4 | export const exportGeneratedUrls = async (req, res) => { 5 | try { 6 | const urls = await UrlModel.find({ userId: req.user._id }); 7 | 8 | if (!urls.length) { 9 | return res.status(404).json({ error: 'No Generated URL found' }); 10 | } 11 | 12 | const workbook = new ExcelJS.Workbook(); 13 | workbook.creator = 'SnapURL'; 14 | workbook.created = new Date(); 15 | 16 | const worksheet = workbook.addWorksheet('Generated_URLs Sheet'); 17 | 18 | worksheet.columns = [ 19 | { header: 'Short URL', key: 'shortUrl', width: 50 }, 20 | { header: 'Original URL', key: 'originalUrl', width: 50 }, 21 | { header: 'Category', key: 'category', width: 20 }, 22 | { header: 'Custom Back Half', key: 'customBackHalf', width: 20 }, 23 | { header: 'Visits', key: 'visits', width: 10 }, 24 | ]; 25 | 26 | worksheet.getRow(1).font = { bold: true }; 27 | 28 | urls.forEach((url) => { 29 | worksheet.addRow({ 30 | shortUrl: `${process.env.SHORT_URL_PREFIX}/${url.shortUrl}`, 31 | originalUrl: url.originalUrl, 32 | category: url.category, 33 | customBackHalf: url.customBackHalf, 34 | visits: url.visitCount, 35 | }); 36 | }); 37 | 38 | const buffer = await workbook.xlsx.writeBuffer(); 39 | 40 | res.setHeader( 41 | 'Content-Type', 42 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 43 | ); 44 | res.setHeader( 45 | 'Content-Disposition', 46 | 'attachment; filename=Generated_URLs.xlsx', 47 | ); 48 | res.status(200).send(buffer); 49 | } catch (error) { 50 | console.error(error); 51 | res.status(500).json({ error: 'Server error' }); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/generateCustomBackHalf.js: -------------------------------------------------------------------------------- 1 | import UrlModel from '../../models/UrlModel.js'; 2 | import { generate } from 'random-words'; 3 | 4 | // Generate a custom back-half of a short URL for a given short URL 5 | export const generateCustomBackHalf = async (req, res) => { 6 | try { 7 | const { backHalf, shortUrl } = req.body; 8 | const userId = req.user._id; 9 | 10 | // check if the custom back-half already exists 11 | const existingUrl = await UrlModel.findOne({ customBackHalf: backHalf }); 12 | 13 | if (existingUrl) { 14 | // generate a random back-half, if custom one already exists 15 | const randomBackHalf = generate({ minLength: 3, maxLength: 10 }); 16 | 17 | return res.status(400).json({ 18 | error: 'Custom back-half already exists', 19 | suggestion: randomBackHalf, 20 | }); 21 | } 22 | 23 | // update the URL with the custom back-half 24 | const updatedUrl = await UrlModel.findOneAndUpdate( 25 | { userId, shortUrl }, 26 | { customBackHalf: backHalf }, 27 | { new: true }, 28 | ); 29 | // console.log('Updated url data:', updatedUrl); 30 | 31 | // If the URL does not exist, return an error 32 | if (!updatedUrl) { 33 | return res.status(400).json({ 34 | error: 'URL does not exist', 35 | }); 36 | } 37 | 38 | // Send response with the success message 39 | res.status(200).json({ 40 | message: 'Custom back-half generated', 41 | customBackHalf: updatedUrl.customBackHalf, 42 | }); 43 | } catch (error) { 44 | console.error(error); 45 | res.status(500).json({ error: 'Server Error' }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/generateShortUrl.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | import UrlModel from '../../models/UrlModel.js'; 3 | 4 | // Generate a short URL for a given long URL 5 | export const generateShortUrl = async (req, res) => { 6 | const SHORT_URL_PREFIX = process.env.SHORT_URL_PREFIX; 7 | try { 8 | const { url } = req.body; 9 | 10 | // Get the user ID from the request 11 | const userId = req.user._id; 12 | 13 | // Get the URL details from the database 14 | const urlDetails = await UrlModel.findOne( 15 | { originalUrl: url, userId }, 16 | { shortUrl: 1, customBackHalf: 1, _id: 0 }, 17 | ); 18 | // console.log(`urlDetails: ${urlDetails}`); 19 | 20 | if (urlDetails) { 21 | // if the URL has already been shortened for the user, return the existing short URL 22 | const { shortUrl, customBackHalf } = urlDetails; 23 | return res.status(200).json({ 24 | shortUrl: `${SHORT_URL_PREFIX}/${shortUrl}`, 25 | customBackHalf: customBackHalf, 26 | }); 27 | } 28 | 29 | // If the URL has not been shortened for the user, generate a new short URL 30 | const id = nanoid(10); 31 | 32 | await UrlModel.create({ 33 | userId, 34 | shortUrl: id, 35 | originalUrl: url, 36 | }); 37 | 38 | // Send response with the generated short URL 39 | res.status(200).json({ 40 | shortUrl: `${SHORT_URL_PREFIX}/${id}`, 41 | customBackHalf: null, 42 | }); 43 | } catch (error) { 44 | console.error(error); 45 | res.status(500).json({ error: 'Server error' }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/getHistory.js: -------------------------------------------------------------------------------- 1 | import UrlModel from '../../models/UrlModel.js'; 2 | 3 | // Get the URL history for a user 4 | export const getHistory = async (req, res) => { 5 | try { 6 | // const urlObj = await UrlModel.findOne({ userId: req.user._id }); 7 | const urlArray = await UrlModel.find({ userId: req.user._id }); 8 | 9 | if (!urlArray.length) { 10 | return res.status(404).json({ error: 'No Generated URL found' }); 11 | } 12 | 13 | res.status(200).json(urlArray); 14 | } catch (error) { 15 | console.error(error); 16 | res.status(500).json({ error: 'Server error' }); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /backend/controllers/urlControllers/redirectToURL.js: -------------------------------------------------------------------------------- 1 | import UrlModel from '../../models/UrlModel.js'; 2 | 3 | // Redirect to the original URL associated with a short URL 4 | export const redirectToOriginalUrl = async (req, res) => { 5 | try { 6 | const { short } = req.params; 7 | 8 | const url = await UrlModel.findOne({ shortUrl: short }); 9 | 10 | // if the URL does not exist 11 | if (!url) { 12 | return res.status(404).json({ error: 'Url not found' }); 13 | } 14 | 15 | // increment the URL visit count 16 | const visitCount = url.visitCount || 0; 17 | await url.updateOne({ visitCount: visitCount + 1 }); 18 | 19 | // Perform a 302 redirect to the original URL 20 | res.status(302).redirect(url.originalUrl); 21 | } catch (error) { 22 | console.error(error); 23 | res.status(500).json({ error: 'Server error' }); 24 | } 25 | }; 26 | 27 | export const redirectViaCustomBackHalf = async (req, res) => { 28 | try { 29 | const { backHalf } = req.params; 30 | 31 | const url = await UrlModel.findOne({ customBackHalf: backHalf }); 32 | 33 | // if the URL does not exist 34 | if (!url) { 35 | return res.status(404).json({ error: 'Url not found' }); 36 | } 37 | 38 | // increment the URL visit count 39 | const visitCount = url.visitCount || 0; 40 | await url.updateOne({ visitCount: visitCount + 1 }); 41 | 42 | // Perform a 302 redirect to the original URL 43 | res.status(302).redirect(url.originalUrl); 44 | } catch (error) { 45 | console.error(error); 46 | res.status(500).json({ error: 'Server error' }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /backend/extras/Constants.js: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | config(); 4 | 5 | export const SHORT_URL_PREFIX = process.env.SHORT_URL_PREFIX; 6 | -------------------------------------------------------------------------------- /backend/extras/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import LocalStrategy from 'passport-local'; 3 | import User from '../models/UserModel.js'; 4 | 5 | // Configure the local strategy for passport 6 | passport.use( 7 | new LocalStrategy( 8 | { usernameField: 'email' }, // use the email field as the username 9 | (email, password, done) => { 10 | // Find the user with the given email 11 | User.findOne({ email: email }, (err, user) => { 12 | if (err) { 13 | return done(err); 14 | } 15 | if (!user) { 16 | return done(null, false, { message: 'Incorrect email.' }); 17 | } 18 | // Check if the password is correct 19 | user.comparePassword(password, (err, isMatch) => { 20 | if (err) { 21 | return done(err); 22 | } 23 | if (!isMatch) { 24 | return done(null, false, { message: 'Incorrect password.' }); 25 | } 26 | return done(null, user); 27 | }); 28 | }); 29 | }, 30 | ), 31 | ); 32 | 33 | passport.serializeUser(function (user, done) { 34 | done(null, user.id); 35 | }); 36 | 37 | passport.deserializeUser(function (id, done) { 38 | User.findById(id, function (err, user) { 39 | done(err, user); 40 | }); 41 | }); 42 | 43 | export default passport; 44 | -------------------------------------------------------------------------------- /backend/jobs/customDomainJobs.js: -------------------------------------------------------------------------------- 1 | import cron from 'node-cron'; 2 | import CustomDomain from '../models/CustomDomainsModel.js'; 3 | import { checkDNSTxtVerification } from '../utils/dns.js'; 4 | 5 | const fetchAndVerifyDomains = () => { 6 | (async () => { 7 | try { 8 | const domains = await CustomDomain.find({ isVerified: { $eq: false } }); 9 | 10 | for (let i = 0; i < domains.length; i++) { 11 | const domain = domains[i]; 12 | 13 | const domainVerification = await checkDNSTxtVerification( 14 | domain.url, 15 | domain.dnsVerificationCode, 16 | ); 17 | 18 | if (domainVerification.status === true) { 19 | // send mail to inform domain verification is successful 20 | domain.isVerified = true; 21 | domain.save(); 22 | } 23 | } 24 | } catch (error) { 25 | if (error.message) { 26 | console.error(error); 27 | } 28 | } 29 | })(); 30 | }; 31 | 32 | const initCustomDomainJobs = () => { 33 | cron.schedule('0 * * * *', fetchAndVerifyDomains); 34 | }; 35 | 36 | export default initCustomDomainJobs; 37 | -------------------------------------------------------------------------------- /backend/middlewares/ValidatorErrorHandler.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from 'express-validator'; 2 | 3 | export const validationErrorHandler = (req, res, next) => { 4 | const errors = validationResult(req); 5 | if (!errors.isEmpty()) { 6 | return res.status(400).json({ errors: errors.array() }); 7 | } 8 | next(); 9 | }; 10 | -------------------------------------------------------------------------------- /backend/middlewares/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | export const isApiAuthenticated = async (req, res, next) => { 4 | // check if token is present in headers 5 | if (!req.headers.authorization) { 6 | res.status(401).json({ error: 'Unauthorized' }); 7 | return; 8 | } 9 | // verify the token 10 | const token = req.headers.authorization.split(' ')[1]; 11 | jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { 12 | if (err) { 13 | res.status(401).json({ error: 'Invalid token' }); 14 | } else { 15 | req.user = decoded; 16 | next(); 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /backend/middlewares/isAdmin.js: -------------------------------------------------------------------------------- 1 | import Admin from '../models/AdminModel.js'; 2 | export const isAdmin = async (req, res, next) => { 3 | try { 4 | const user = await Admin.findById(req.user._id); 5 | 6 | if (!user) { 7 | return res 8 | .status(401) 9 | .json({ error: 'adminControllers resource. Access denied' }); 10 | } 11 | 12 | // If user is an adminControllers, set it in req.profile 13 | req.profile = user; 14 | next(); 15 | } catch (error) { 16 | console.error(error); 17 | return res.status(500).json({ error: 'Server error' }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/models/AdminModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const saltRounds = 10; // Affects the performance and password security level 5 | 6 | const AdminSchema = new mongoose.Schema( 7 | { 8 | email: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | }, 13 | 14 | password: { 15 | type: String, 16 | required: true, 17 | }, 18 | 19 | name: { 20 | type: String, 21 | required: true, 22 | }, 23 | 24 | isEmailVerified: { 25 | type: Boolean, 26 | default: false, // Initially, the email is not verified 27 | }, 28 | }, 29 | { timestamps: true }, 30 | ); 31 | 32 | // Hash the password before saving 33 | AdminSchema.pre('save', function (next) { 34 | const user = this; 35 | if (!user.isModified('password')) return next(); 36 | 37 | bcrypt.genSalt(saltRounds, function (err, salt) { 38 | if (err) return next(err); 39 | 40 | bcrypt.hash(user.password, salt, (err, hash) => { 41 | if (err) return next(err); 42 | 43 | user.password = hash; 44 | next(); 45 | }); 46 | }); 47 | }); 48 | 49 | // Compare the password 50 | AdminSchema.methods.comparePassword = function (password, callback) { 51 | bcrypt.compare(password, this.password, (err, isMatch) => { 52 | if (err) return callback(err); 53 | callback(null, isMatch); 54 | }); 55 | }; 56 | 57 | const Admin = mongoose.model('Admin', AdminSchema); 58 | 59 | export default Admin; 60 | -------------------------------------------------------------------------------- /backend/models/CustomDomainsModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const customDomainSchema = new mongoose.Schema({ 4 | url: { 5 | type: String, 6 | required: [true, 'Please provide a valid url value'], 7 | }, 8 | dnsVerificationCode: { 9 | type: String, 10 | default: null, 11 | }, 12 | isVerified: { 13 | type: Boolean, 14 | default: false, 15 | }, 16 | user: { 17 | type: mongoose.Schema.Types.ObjectId, 18 | ref: 'User', 19 | }, 20 | }); 21 | 22 | const CustomDomain = mongoose.model('CustomDomain', customDomainSchema); 23 | 24 | export default CustomDomain; 25 | -------------------------------------------------------------------------------- /backend/models/Feedback.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const feedbackSchema = new mongoose.Schema( 3 | { 4 | email: { 5 | type: String, 6 | required: true, 7 | }, 8 | name: { 9 | type: String, 10 | required: true, 11 | }, 12 | message: String, 13 | rating: Number, 14 | }, 15 | { 16 | timestamps: true, 17 | }, 18 | ); 19 | 20 | const Feedback = mongoose.model('Feedback', feedbackSchema); 21 | 22 | // Middleware to parse JSON data 23 | 24 | export default Feedback; 25 | -------------------------------------------------------------------------------- /backend/models/LinkInBioPageModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const LinkInBioPageSchema = new mongoose.Schema({ 4 | userId: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'User', 7 | required: true, 8 | index: true, 9 | }, 10 | pageTitle: { 11 | type: String, 12 | required: true, 13 | }, 14 | links: [ 15 | { 16 | title: String, 17 | url: String, 18 | description: String, 19 | }, 20 | ], 21 | }); 22 | 23 | const LinkInBioPage = mongoose.model('LinkInBioPage', LinkInBioPageSchema); 24 | 25 | export default LinkInBioPage; 26 | -------------------------------------------------------------------------------- /backend/models/Tokenmodel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const tokenSchema = new mongoose.Schema( 4 | { 5 | userId: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', // Reference to the User model 8 | required: true, 9 | }, 10 | token: { 11 | type: String, 12 | required: true, 13 | }, 14 | expiration: { 15 | type: Date, 16 | required: true, 17 | }, 18 | }, 19 | { timestamps: true }, 20 | ); 21 | 22 | const TokenModel = mongoose.model('Token', tokenSchema); 23 | 24 | export default TokenModel; 25 | -------------------------------------------------------------------------------- /backend/models/UrlModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const urlSchema = new mongoose.Schema( 4 | { 5 | userId: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | index: true, 9 | }, 10 | 11 | shortUrl: { 12 | type: String, 13 | index: true, 14 | uniuque: true, 15 | }, 16 | originalUrl: { type: String }, 17 | category: { 18 | type: String, 19 | index: true, 20 | }, 21 | visitCount: { 22 | type: Number, 23 | default: 0, 24 | index: { order: -1 }, // this will help to get top visited urls, for sorting 25 | }, 26 | customBackHalf: { 27 | type: String, 28 | }, 29 | }, 30 | { 31 | timestamps: true, 32 | }, 33 | ); 34 | 35 | const UrlModel = new mongoose.model('url_collection', urlSchema); 36 | 37 | export default UrlModel; 38 | -------------------------------------------------------------------------------- /backend/models/UserModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const saltRounds = 10; // Affects the performance and password security level 5 | 6 | const UserSchema = new mongoose.Schema( 7 | { 8 | email: { 9 | type: String, 10 | required: true, 11 | unique: true, 12 | }, 13 | 14 | password: { 15 | type: String, 16 | required: true, 17 | }, 18 | 19 | name: { 20 | type: String, 21 | required: true, 22 | }, 23 | 24 | isEmailVerified: { 25 | type: Boolean, 26 | default: false, // Initially, the email is not verified 27 | }, 28 | customDomain: { 29 | type: mongoose.Schema.Types.ObjectId, 30 | ref: 'CustomDomain', 31 | default: null, 32 | select: false, 33 | }, 34 | }, 35 | 36 | { timestamps: true }, 37 | ); 38 | 39 | // Hash the password before saving 40 | UserSchema.pre('save', function (next) { 41 | const user = this; 42 | if (!user.isModified('password')) return next(); 43 | 44 | bcrypt.genSalt(saltRounds, function (err, salt) { 45 | if (err) return next(err); 46 | 47 | bcrypt.hash(user.password, salt, (err, hash) => { 48 | if (err) return next(err); 49 | 50 | user.password = hash; 51 | next(); 52 | }); 53 | }); 54 | }); 55 | 56 | // Compare the password 57 | UserSchema.methods.comparePassword = function (password, callback) { 58 | bcrypt.compare(password, this.password, (err, isMatch) => { 59 | if (err) return callback(err); 60 | callback(null, isMatch); 61 | }); 62 | }; 63 | 64 | // Add a reference to the LinkInBioPage model 65 | UserSchema.virtual('linkInBioPage', { 66 | ref: 'LinkInBioPage', 67 | localField: '_id', 68 | foreignField: 'userId', 69 | justOne: true, 70 | }); 71 | 72 | const User = mongoose.model('User', UserSchema); 73 | 74 | export default User; 75 | -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "env": { 9 | "NODE_ENV": "development" 10 | }, 11 | "ext": "js,json,html,css, yml", 12 | "delay": "2500" 13 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "url-shortener", 3 | "version": "1.6.0", 4 | "description": "URL Shortener using Node and Express", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "dev": "nodemon index.js", 9 | "format": "prettier --write \"**/*.{js,jsx}\"", 10 | "test": "mocha --timeout 10000" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice.git" 15 | }, 16 | "keywords": [ 17 | "urlshort", 18 | "shorturl", 19 | "urlshortener" 20 | ], 21 | "author": "Dhananjay Thomble", 22 | "bugs": { 23 | "url": "https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice/issues" 24 | }, 25 | "homepage": "https://github.com/DhananjayThomble/NodeJS-URL-Shortener-Microservice#readme", 26 | "dependencies": { 27 | "aws-sdk": "^2.1691.0", 28 | "bcrypt": "^5.1.0", 29 | "bcryptjs": "^2.4.3", 30 | "body-parser": "^1.20.1", 31 | "cors": "^2.8.5", 32 | "crypto": "^1.0.1", 33 | "dotenv": "^16.0.3", 34 | "ejs": "^3.1.9", 35 | "exceljs": "^4.3.0", 36 | "express": "^4.18.2", 37 | "express-rate-limit": "^6.7.0", 38 | "express-session": "^1.17.3", 39 | "express-validator": "^7.0.1", 40 | "jsonwebtoken": "^9.0.0", 41 | "mongoose": "^6.8.0", 42 | "nanoid": "^4.0.0", 43 | "node-cron": "^3.0.2", 44 | "nodemailer": "^6.9.5", 45 | "passport": "^0.6.0", 46 | "passport-jwt": "^4.0.1", 47 | "passport-local": "^1.0.0", 48 | "random-words": "^2.0.0", 49 | "swagger-ui-express": "^4.6.0", 50 | "winston": "^3.14.2", 51 | "winston-cloudwatch": "^6.3.0", 52 | "yamljs": "^0.3.0" 53 | }, 54 | "type": "module", 55 | "devDependencies": { 56 | "chai": "^4.3.10", 57 | "chai-http": "^4.4.0", 58 | "mocha": "^10.2.0", 59 | "nodemon": "^3.0.1", 60 | "prettier": "^3.0.3", 61 | "supertest": "^6.3.3" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/routes/admin.route.js: -------------------------------------------------------------------------------- 1 | import Router from 'express'; 2 | 3 | const router = Router(); 4 | 5 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js'; 6 | import { isAdmin } from '../middlewares/isAdmin.js'; 7 | import { getReviews } from '../controllers/feedbackControllers/reviews.js'; 8 | import { 9 | deleteAllFeedback, 10 | deleteReview, 11 | } from '../controllers/adminControllers/manageReviews.js'; 12 | 13 | // Feedback Routes 14 | router.get('/reviews', isApiAuthenticated, isAdmin, getReviews); 15 | router.delete('/reviews/:_id', isApiAuthenticated, isAdmin, deleteReview); 16 | router.delete('/reviews', isApiAuthenticated, isAdmin, deleteAllFeedback); 17 | 18 | export default router; 19 | -------------------------------------------------------------------------------- /backend/routes/adminAuth.route.js: -------------------------------------------------------------------------------- 1 | import Router from 'express'; 2 | 3 | const router = Router(); 4 | 5 | import { 6 | forgetPasswordValidator, 7 | validateLogin, 8 | validateSignup, 9 | } from '../validators/authValidators.js'; 10 | 11 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js'; 12 | import { adminLogin } from '../controllers/adminControllers/adminLogin.js'; 13 | import { adminSignup } from '../controllers/adminControllers/adminSignup.js'; 14 | import { adminForgotPassword } from '../controllers/adminControllers/adminForgotPassword.js'; 15 | import { adminResetPassword } from '../controllers/adminControllers/adminResetPassword.js'; 16 | import { adminVerifyEmail } from '../controllers/adminControllers/adminVerifyEmail.js'; 17 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js'; 18 | 19 | // Auth Routes 20 | router.post('/login', validateLogin, validationErrorHandler, adminLogin); 21 | router.post('/signup', validateSignup, validationErrorHandler, adminSignup); 22 | router.post( 23 | '/forgot-password', 24 | forgetPasswordValidator, 25 | validationErrorHandler, 26 | adminForgotPassword, 27 | ); //For Sending the password reset request 28 | router.post('/reset-password', adminResetPassword); // For Reseting the password 29 | router.get('/verify-email', adminVerifyEmail); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /backend/routes/domain.route.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js'; 3 | import { addCustomDomain } from '../controllers/domainControllers/addCustomDomain.js'; 4 | 5 | const router = Router(); 6 | 7 | router.use(isApiAuthenticated); 8 | 9 | router.post('/add-custom-domain', addCustomDomain); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /backend/routes/url.route.js: -------------------------------------------------------------------------------- 1 | import Router from 'express'; 2 | 3 | const router = Router(); 4 | 5 | import { redirectToOriginalUrl } from '../controllers/urlControllers/redirectToURL.js'; 6 | import { deleteUrl } from '../controllers/urlControllers/deleteUrl.js'; 7 | import { getHistory } from '../controllers/urlControllers/getHistory.js'; 8 | import { generateShortUrl } from '../controllers/urlControllers/generateShortUrl.js'; 9 | import { exportGeneratedUrls } from '../controllers/urlControllers/exportGeneratedUrls.js'; 10 | import { generateCustomBackHalf } from '../controllers/urlControllers/generateCustomBackHalf.js'; 11 | import { validateUrl } from '../validators/urlValidator.js'; 12 | 13 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js'; 14 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js'; 15 | import { 16 | getFilteredCategory, 17 | updateCategory, 18 | } from '../controllers/urlControllers/category.js'; 19 | 20 | // old route for backward compatibility 21 | router.get('/url/:short', redirectToOriginalUrl); 22 | 23 | router.post( 24 | '/url', 25 | isApiAuthenticated, 26 | validateUrl, 27 | validationErrorHandler, 28 | generateShortUrl, 29 | ); 30 | 31 | router.post('/url/custom', isApiAuthenticated, generateCustomBackHalf); 32 | 33 | router.get('/history', isApiAuthenticated, getHistory); 34 | 35 | router.delete('/delete/:id', isApiAuthenticated, deleteUrl); 36 | 37 | router.get('/export', isApiAuthenticated, exportGeneratedUrls); 38 | 39 | router.get('/filter/:category', isApiAuthenticated, getFilteredCategory); 40 | 41 | router.put('/filter', isApiAuthenticated, updateCategory); 42 | 43 | export default router; 44 | -------------------------------------------------------------------------------- /backend/routes/userAuth.route.js: -------------------------------------------------------------------------------- 1 | import Router from 'express'; 2 | 3 | const router = Router(); 4 | 5 | import { isApiAuthenticated } from '../middlewares/authMiddleware.js'; 6 | 7 | import { login } from '../controllers/authControllers/login.js'; 8 | import { signup } from '../controllers/authControllers/signup.js'; 9 | import { forgotPassword } from '../controllers/authControllers/forgotPassword.js'; 10 | import { resetPassword } from '../controllers/authControllers/resetPassword.js'; 11 | import { verifyEmail } from '../controllers/authControllers/verifyEmail.js'; 12 | import { 13 | validateLogin, 14 | validateSignup, 15 | forgetPasswordValidator, 16 | } from '../validators/authValidators.js'; 17 | 18 | import { validationErrorHandler } from '../middlewares/ValidatorErrorHandler.js'; 19 | 20 | import { validateFeedback } from '../validators/feedbackValidator.js'; 21 | import { submitFeedback } from '../controllers/feedbackControllers/feedback.js'; 22 | 23 | import { changeEmail } from '../controllers/authControllers/changeEmail.js'; 24 | import { getCurrentUser } from '../controllers/authControllers/getCurrentUser.js'; 25 | import { changeName } from '../controllers/authControllers/changeName.js'; 26 | import { validateEmail } from '../validators/authValidators.js'; 27 | import { validateName } from '../validators/authValidators.js'; 28 | import { validateToken } from '../validators/authValidators.js'; 29 | 30 | // Auth Routes 31 | router.post('/login', validateLogin, validationErrorHandler, login); 32 | router.post('/signup', validateSignup, validationErrorHandler, signup); 33 | router.post( 34 | '/forgot-password', 35 | forgetPasswordValidator, 36 | validationErrorHandler, 37 | forgotPassword, 38 | ); //For Sending the password reset request 39 | router.post('/reset-password', resetPassword); // For Reseting the password 40 | router.get('/verify-email', validateToken, validationErrorHandler, verifyEmail); 41 | 42 | // Profile Routes 43 | router.get('/current-user', getCurrentUser); 44 | router.patch( 45 | '/change-email', 46 | validateEmail, 47 | validationErrorHandler, 48 | changeEmail, 49 | ); 50 | router.patch('/change-name', validateName, validationErrorHandler, changeName); 51 | 52 | /*Feedback Routes*/ 53 | router.post( 54 | '/feedback', 55 | isApiAuthenticated, 56 | validateFeedback, 57 | validationErrorHandler, 58 | submitFeedback, 59 | ); 60 | 61 | export default router; 62 | -------------------------------------------------------------------------------- /backend/test/mainTest.js: -------------------------------------------------------------------------------- 1 | import { app } from '../index.js'; 2 | 3 | export { app }; 4 | -------------------------------------------------------------------------------- /backend/test/urlController.test.js: -------------------------------------------------------------------------------- 1 | import chaiHttp from 'chai-http'; 2 | import chai from 'chai'; 3 | import { expect } from 'chai'; 4 | import dotenv from 'dotenv'; 5 | import { app } from './mainTest.js'; 6 | 7 | dotenv.config(); 8 | 9 | chai.use(chaiHttp); 10 | 11 | const MOCK_TOKEN = 12 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFlZTBlOGQ4NTQ3NDkyNGVlYTU2ZmIiLCJpYXQiOjE2OTcxMjYzMDIsImV4cCI6MTY5NzczMTEwMn0.FbNfFFiG0biv_4kD16h8OVXqUcvsEksh5iRFusACKhI'; 13 | 14 | const EXPIRED_TOKEN = 15 | 'abchbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFlZTBlOGQ4NTQ3NDkyNGVlYTU2ZmIiLCJpYXQiOjE2OTcxMjYzMDIsImV4cCI6MTY5NzczMTEwMn0.FbNfFFiG0biv_4kD16h8OVXqUcvsEksh5iRFusACKhI'; 16 | 17 | describe('URL Controller Tests', () => { 18 | it('should generate a short URL with a valid URL and token', (done) => { 19 | chai 20 | .request(app) 21 | .post(`/api/url`) 22 | .set('Authorization', 'Bearer ' + MOCK_TOKEN) 23 | .send({ url: 'https://dturl.live/' }) 24 | .end((err, res) => { 25 | expect(err).to.be.null; // Check for no errors 26 | expect(res).to.have.status(200); // Check for the expected HTTP status 27 | expect(res.body).to.have.property('shortUrl'); // Check for the expected response properties 28 | done(); // call done() to indicate that the test is complete 29 | }); 30 | }); 31 | 32 | it('should return an error for an invalid URL', (done) => { 33 | chai 34 | .request(app) 35 | .post(`/api/url`) 36 | .set('Authorization', 'Bearer ' + MOCK_TOKEN) 37 | .send({ url: 'invalid-url' }) 38 | .end((err, res) => { 39 | expect(err).to.be.null; 40 | expect(res).to.have.status(400); 41 | // we can check for an error response, e.g., `expect(res.body).to.have.property("error")` 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should return an error if no token is provided', (done) => { 47 | chai 48 | .request(app) 49 | .post(`/api/url`) 50 | .send({ url: 'https://dturl.live/' }) 51 | .end((err, res) => { 52 | expect(err).to.be.null; 53 | expect(res).to.have.status(401); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should return an error if an expired token is used', (done) => { 59 | chai 60 | .request(app) 61 | .post(`/api/url`) 62 | .set('Authorization', 'Bearer ' + EXPIRED_TOKEN) 63 | .send({ url: 'https://dturl.live/' }) 64 | .end((err, res) => { 65 | expect(err).to.be.null; 66 | expect(res).to.have.status(401); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | 72 | // dummy test 73 | describe('Dummy Test', () => { 74 | it('should return true', (done) => { 75 | expect(true).to.be.true; 76 | done(); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /backend/utils/dns.js: -------------------------------------------------------------------------------- 1 | import dns from 'node:dns'; 2 | 3 | /** 4 | * Verifies DNS records for a given hostname against a verification code 5 | * 6 | * @param {String} hostname - hostname to check the verification against 7 | * @param {String} text - verification code for the domain 8 | * @returns Promise. The resolved object has the following structure: 9 | * - status (boolean): Indicates if verification was successful 10 | * - message (string): A message describing the verification status 11 | */ 12 | export const checkDNSTxtVerification = (hostname, text) => { 13 | return new Promise((resolve, reject) => { 14 | if (!hostname) { 15 | return reject(new Error('Please provide a hostname')); 16 | } 17 | 18 | dns.resolveTxt(hostname, (error, results) => { 19 | if (error) { 20 | return reject(error); 21 | } 22 | 23 | if (!results || results.length === 0) { 24 | reject({ 25 | status: false, 26 | message: 'Verification code not present in the DNS records', 27 | }); 28 | } 29 | 30 | let containsCode = false; 31 | 32 | results.forEach((result) => { 33 | if (result.includes(text)) { 34 | containsCode = true; 35 | } 36 | }); 37 | 38 | if (containsCode) { 39 | resolve({ status: true, message: 'Verification successful' }); 40 | } 41 | 42 | reject({ 43 | status: false, 44 | message: 'Wrong verification code provided', 45 | }); 46 | }); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /backend/utils/helperfunc.js: -------------------------------------------------------------------------------- 1 | export const isValidEmail = (email) => { 2 | validEmailRegex = /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/gi; 3 | return validEmailRegex.test(email); 4 | }; 5 | -------------------------------------------------------------------------------- /backend/utils/mailSend.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import TokenModel from '../models/Tokenmodel.js'; 3 | import dotenv from 'dotenv'; 4 | import crypto from 'crypto'; 5 | import fs from 'fs'; 6 | import ejs from 'ejs'; 7 | dotenv.config(); 8 | 9 | const transporter = nodemailer.createTransport({ 10 | host: process.env.EMAIL_HOST, 11 | port: process.env.EMAIL_PORT, 12 | secure: true, 13 | requireTLS: true, 14 | auth: { 15 | user: process.env.EMAIL_HOST_USER, 16 | pass: process.env.EMAIL_HOST_PASSWORD, 17 | }, 18 | }); 19 | 20 | export const sendEmail = async (options) => { 21 | return new Promise((resolve, reject) => { 22 | try { 23 | const subject = options.subject; 24 | const recipient = options.recipient; 25 | const html = options.html; 26 | 27 | const mailOptions = { 28 | from: options.from, 29 | to: recipient, 30 | subject: subject, 31 | html: html, 32 | }; 33 | 34 | transporter.sendMail(mailOptions, (error, info) => { 35 | if (error) { 36 | console.error(`Error from mailSender ${error}`); 37 | reject(error); 38 | } else { 39 | console.log(`Email sent: ${info.response}`); 40 | resolve(info); 41 | } 42 | }); 43 | } catch (error) { 44 | console.error(`Error from mailSender ${error}`); 45 | reject(error); 46 | } 47 | }); 48 | }; 49 | 50 | // <--------------------Sending email to the user -----------------------------------> 51 | 52 | export const sendWelcomeEmail = async (name, email, userID) => { 53 | try { 54 | console.log('Sending welcome email to', email); 55 | const verifyEmailTemplate = fs.readFileSync( 56 | './views/welcome_email_template.ejs', 57 | 'utf-8', 58 | ); 59 | 60 | const dataToRender = { 61 | name: name, 62 | verificationLink: await getVerificationLink(userID), 63 | }; 64 | 65 | const htmlTemplate = ejs.render(verifyEmailTemplate, dataToRender); 66 | 67 | const options = { 68 | from: 'SnapURL@snapurl.in', 69 | subject: 'Welcome to SnapURL🔗', 70 | recipient: email, 71 | html: htmlTemplate, 72 | }; 73 | 74 | await sendEmail(options); 75 | } catch (err) { 76 | console.log('Error sending welcome email to', email); 77 | console.error(err); 78 | } 79 | }; 80 | 81 | const getVerificationLink = async (userId) => { 82 | try { 83 | const token = crypto.randomBytes(32).toString('hex'); // Generate a random token 84 | const expiration = Date.now() + 24 * 60 * 60 * 1000; // Token expires in 24 hours 85 | 86 | const verificationToken = new TokenModel({ 87 | userId, 88 | token, 89 | expiration, 90 | }); 91 | 92 | await verificationToken.save(); 93 | 94 | // const verificationLink = `http://localhost:4001/auth/verify-email?token=${token}`; 95 | const verificationLink = `${process.env.BASE_URL}/auth/verify-email?token=${token}`; 96 | return verificationLink; 97 | } catch (error) { 98 | console.error(error); 99 | return null; 100 | } 101 | }; 102 | 103 | export const sendPasswordResetEmail = async (email, resetLink) => { 104 | try { 105 | const resetPasswordTemplate = fs.readFileSync( 106 | './views/reset_password_email_template.ejs', 107 | 'utf-8', 108 | ); 109 | 110 | const dataToRender = { 111 | resetLink: resetLink, 112 | }; 113 | 114 | const htmlTemplate = ejs.render(resetPasswordTemplate, dataToRender); 115 | 116 | const options = { 117 | from: 'SnapURL@snapurl.in', 118 | subject: 'Reset your SnapURL password', 119 | recipient: email, 120 | html: htmlTemplate, 121 | }; 122 | 123 | await sendEmail(options); 124 | } catch (err) { 125 | console.log('Error sending password reset email to', email); 126 | console.error(err); 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /backend/validators/authValidators.js: -------------------------------------------------------------------------------- 1 | import { body, query } from 'express-validator'; 2 | import User from '../models/UserModel.js'; 3 | 4 | export const validateLogin = [ 5 | body('email') 6 | .notEmpty() 7 | .trim() 8 | .escape() 9 | .isEmail() 10 | .withMessage('Invalid email'), 11 | body('password') 12 | .notEmpty() 13 | .trim() 14 | .escape() 15 | .withMessage('Password is required'), 16 | ]; 17 | 18 | export const validateSignup = [ 19 | body('name') 20 | .notEmpty() 21 | .trim() 22 | .escape() 23 | .withMessage('Name is required') 24 | .bail() 25 | .isLength({ min: 3 }) 26 | .withMessage('Name must be at least 3 characters long'), 27 | 28 | body('email') 29 | .notEmpty() 30 | .trim() 31 | .escape() 32 | .isEmail() 33 | .withMessage('Invalid email format') 34 | .bail() 35 | .custom(async (value) => { 36 | const user = await User.findOne({ email: value }); 37 | if (user) { 38 | throw new Error('Email already in use'); 39 | } 40 | }), 41 | // body("mobileNumber") 42 | // .notEmpty() 43 | // .trim() 44 | // .escape() 45 | // .isMobilePhone("en-IN") 46 | // .withMessage("Invalid mobile number format") 47 | // .bail() 48 | // .custom(async (value) => { 49 | // const user = await User.findOne({ mobileNumber: value }); 50 | // if (user) { 51 | // throw new Error("Mobile number already in use"); 52 | // } 53 | // }), 54 | body('password') 55 | .notEmpty() 56 | .trim() 57 | .escape() 58 | .withMessage('Password is required') 59 | .isLength({ min: 6 }) 60 | .withMessage('Password must be at least 6 characters long'), 61 | // body("confirmPassword") 62 | // .notEmpty() 63 | // .trim() 64 | // .escape() 65 | // .withMessage("Confirm password is required") 66 | // .bail() 67 | // .custom((value, { req }) => { 68 | // if (value !== req.body.password) { 69 | // throw new Error("Passwords do not match"); 70 | // } 71 | // return true; 72 | // }), 73 | ]; 74 | 75 | // validator for the forgot password 76 | export const forgetPasswordValidator = [ 77 | body('email') 78 | .notEmpty() 79 | .withMessage('Email is required') 80 | .isEmail() 81 | .withMessage('Invalid Email Address'), 82 | ]; 83 | 84 | export const validateName = [ 85 | body('name') 86 | .notEmpty() 87 | .trim() 88 | .escape() 89 | .withMessage('Name is required') 90 | .bail() 91 | .isLength({ min: 3 }) 92 | .withMessage('Name must be at least 3 characters long'), 93 | ]; 94 | 95 | export const validateEmail = [ 96 | body('email') 97 | .notEmpty() 98 | .trim() 99 | .escape() 100 | .withMessage('Email is required') 101 | .bail() 102 | .isEmail() 103 | .withMessage('Invalid email format') 104 | .bail() 105 | .custom(async (value) => { 106 | const user = await User.findOne({ email: value }); 107 | if (user) { 108 | throw new Error('Email already in use'); 109 | } 110 | }), 111 | ]; 112 | 113 | export const validateToken = [ 114 | query('token').notEmpty().trim().escape().withMessage('Token is required'), 115 | ]; 116 | -------------------------------------------------------------------------------- /backend/validators/feedbackValidator.js: -------------------------------------------------------------------------------- 1 | import { body } from 'express-validator'; 2 | export const validateFeedback = [ 3 | body('message').notEmpty().trim().escape().withMessage('Message is required'), 4 | body('rating') 5 | .notEmpty() 6 | .trim() 7 | .escape() 8 | .withMessage('Rating is required') 9 | .isInt({ min: 1, max: 5 }) 10 | .withMessage('Rating must be between 1 and 5'), 11 | ]; 12 | -------------------------------------------------------------------------------- /backend/validators/urlValidator.js: -------------------------------------------------------------------------------- 1 | import { check } from 'express-validator'; 2 | export const validateUrl = [check('url').isURL().withMessage('Invalid URL')]; 3 | 4 | export const validateShortId = [ 5 | check('short').isLength({ min: 10, max: 10 }).withMessage('Invalid URL'), 6 | ]; 7 | -------------------------------------------------------------------------------- /backend/views/reset_password_email_template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Reset Your Password 9 | 10 | 49 | 50 | 51 | 52 |
53 | SnapURL 55 |
56 |
57 |

Reset Your Password

58 |

You are receiving this email because we received a password reset request for your SnapURL account. If 59 | you 60 | did not request this change, please ignore this email.

61 |

Click the button below to reset your password. If you're having trouble, copy and paste the URL below 62 | into 63 | your web browser:

64 |

Reset 66 | Your Password

67 |
68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /backend/views/welcome_email_template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to SnapURL 8 | 47 | 48 | 49 | 50 |
51 |
52 | SnapURL 53 |
54 |

Welcome to SnapURL, <%= name %>!

55 |

Your URL Shortening Platform 🚀

56 |

57 | Thank you for joining SnapURL. We're thrilled to have you on board. Get started now by shortening your URLs 58 | and managing your links with ease. 59 |

60 |

61 | To complete your registration, please verify your email address by clicking the following link: 62 | Verify 63 | Email 64 |

65 |
66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /chrome-extension/extension.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 16 | 17 | 78 | 79 | 80 |
81 | 82 |

Paste URL here ⬇️

83 | 84 | 93 | 94 | 97 | 98 | 99 |
100 | 108 |
109 | 117 |
118 |
119 | 120 |
121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /chrome-extension/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | URL Shortener History 15 | 16 | 17 |

URL Shortener History

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Short URLOriginal URLVisit Count
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /chrome-extension/history.js: -------------------------------------------------------------------------------- 1 | const token = 2 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFjZDJiNTI1YTUwOGIwODQ0NmQzYWIiLCJpYXQiOjE2OTYzODc3ODQsImV4cCI6MTY5Njk5MjU4NH0.j1ubpVSrqfm9DhTUYQ4xBhZJj1WYHY1E5WmAYX4HnBM"; 3 | 4 | document.addEventListener("DOMContentLoaded", async function () { 5 | const historyTable = document.getElementById("historyTable"); 6 | 7 | try { 8 | // Fetch history data from your server 9 | const response = await fetch("https://dturl.live/api/history", { 10 | method: "GET", 11 | headers: { 12 | "Content-Type": "application/json", 13 | Authorization: `Bearer ${token}`, // Replace with your actual authorization token 14 | }, 15 | }); 16 | 17 | if (!response.ok) { 18 | throw new Error("Error fetching history data."); 19 | } 20 | 21 | const historyData = await response.json(); 22 | console.log(historyData.urlArray); 23 | // Function to populate the table with history data 24 | function populateTable() { 25 | historyData.urlArray.forEach((entry) => { 26 | const row = document.createElement("tr"); 27 | 28 | const shortUrlCell = document.createElement("td"); 29 | shortUrlCell.textContent = `https://dturl.live/u/${entry.shortUrl}`; 30 | 31 | const originalUrlCell = document.createElement("td"); 32 | originalUrlCell.textContent = entry.originalUrl; 33 | 34 | const visitCountCell = document.createElement("td"); 35 | visitCountCell.textContent = entry.visitCount; 36 | 37 | row.appendChild(shortUrlCell); 38 | row.appendChild(originalUrlCell); 39 | row.appendChild(visitCountCell); 40 | 41 | historyTable.appendChild(row); 42 | }); 43 | } 44 | 45 | // Call the populateTable function when the page loads 46 | populateTable(); 47 | } catch (error) { 48 | console.error(error); 49 | alert("An error occurred while fetching history data."); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /chrome-extension/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/chrome-extension/icon.png -------------------------------------------------------------------------------- /chrome-extension/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/chrome-extension/logo.png -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quick URL Shortener", 3 | "description": "Quickly shorten URLs without leaving the page you're on.", 4 | "version": "1.0", 5 | "manifest_version": 3, 6 | "action": { 7 | "default_popup": "extension.html", 8 | "default_icon": "icon.png" 9 | }, 10 | "permissions": [ 11 | "activeTab" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /chrome-extension/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const form = document.querySelector("form"); 3 | const urlInput = form.querySelector("#inputUrl"); 4 | const shortUrl = form.querySelector("#shortUrl"); 5 | const copyBtn = form.querySelector("#copyBtn"); 6 | const token = 7 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NTFjZDJiNTI1YTUwOGIwODQ0NmQzYWIiLCJpYXQiOjE2OTYzODc3ODQsImV4cCI6MTY5Njk5MjU4NH0.j1ubpVSrqfm9DhTUYQ4xBhZJj1WYHY1E5WmAYX4HnBM"; 8 | form.addEventListener("submit", async (e) => { 9 | e.preventDefault(); 10 | const longUrl = urlInput.value.trim(); // Trim any leading/trailing spaces 11 | 12 | if (!longUrl) { 13 | // Check if the URL input is empty 14 | alert("Please enter a valid URL."); 15 | return; 16 | } 17 | 18 | const data = { url: longUrl }; 19 | 20 | try { 21 | const response = await fetch("https://dturl.live/api/url", { 22 | method: "POST", 23 | headers: { 24 | "Content-Type": "application/json", 25 | Authorization: `Bearer ${token}`, 26 | }, 27 | body: JSON.stringify(data), 28 | }); 29 | 30 | if (!response.ok) { 31 | throw new Error("Error shortening URL."); 32 | } 33 | 34 | const responseData = await response.json(); 35 | 36 | shortUrl.value = responseData.shortUrl; 37 | shortUrl.style.display = "block"; 38 | copyBtn.style.display = "block"; 39 | } catch (error) { 40 | console.error(error); 41 | alert("An error occurred while shortening the URL."); 42 | } 43 | }); 44 | 45 | // Copy to clipboard functionality (no changes needed) 46 | copyBtn.addEventListener("click", () => { 47 | shortUrl.select(); 48 | document.execCommand("copy"); 49 | copyBtn.innerHTML = "Copied to clipboard"; 50 | }); 51 | 52 | // Get current tab URL 53 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 54 | const url = tabs[0].url; 55 | urlInput.value = url; 56 | }); 57 | 58 | // Add a click event listener to the historyBtn 59 | historyBtn.addEventListener("click", async () => { 60 | // try { 61 | // const response = await fetch("https://dturl.live/api/history", { 62 | // method: "GET", 63 | // headers: { 64 | // "Content-Type": 'application/json', 65 | // Authorization: `Bearer ${token}`, 66 | // }, 67 | // }); 68 | 69 | // if (!response.ok) { 70 | // throw new Error("Error fetching history."); 71 | // } 72 | 73 | // const responseData = await response.json(); 74 | // console.log(responseData) 75 | // // Open the history in a new tab 76 | chrome.tabs.create({ url: chrome.runtime.getURL('history.html')}); 77 | // } catch (error) { 78 | // console.error(error); 79 | // alert("An error occurred while fetching the history."); 80 | // } 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /designs/User-Profile-Page/Desktop - User Page - Favorites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Favorites.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/Desktop - User Page - Links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Links.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/Desktop - User Page - Profile edit Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/Desktop - User Page - Profile edit Settings_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Desktop - User Page - Profile edit Settings_2.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/FigmaLink.txt: -------------------------------------------------------------------------------- 1 | https://www.figma.com/file/3rm1By9lxmClZZaaWaU7tf/URL-Shortener-App?type=design&node-id=0%3A1&mode=design&t=tW1Yh7TdGDrBApbB-1 2 | -------------------------------------------------------------------------------- /designs/User-Profile-Page/Mobile - User Page - Favourites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Favourites.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/Mobile - User Page - Links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Links.png -------------------------------------------------------------------------------- /designs/User-Profile-Page/Mobile - User Page - Profile Edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/User-Profile-Page/Mobile - User Page - Profile Edit.png -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 1 (no description).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1 (no description).pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 1 (no description).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1 (no description).png -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1.pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 1.png -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 2 (no description).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2 (no description).pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 2 (no description).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2 (no description).png -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2.pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/desktop/desktop 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/desktop/desktop 2.png -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 1 (no description).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1 (no description).pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 1 (no description).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1 (no description).png -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1.pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 1.png -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 2 (no description).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2 (no description).pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 2 (no description).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2 (no description).png -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2.pdf -------------------------------------------------------------------------------- /designs/profile-finder-page/mobile/mobile 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/profile-finder-page/mobile/mobile 2.png -------------------------------------------------------------------------------- /designs/public-profile-page/desktop 1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop 1.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/desktop/desktop 1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop/desktop 1.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/desktop/desktop2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop/desktop2.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/desktop2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/desktop2.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/mobile/mobile1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile1.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/mobile/mobile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile1.png -------------------------------------------------------------------------------- /designs/public-profile-page/mobile/mobile2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile2.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/mobile/mobile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile/mobile2.png -------------------------------------------------------------------------------- /designs/public-profile-page/mobile1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile1.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/mobile1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile1.png -------------------------------------------------------------------------------- /designs/public-profile-page/mobile2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile2.pdf -------------------------------------------------------------------------------- /designs/public-profile-page/mobile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/designs/public-profile-page/mobile2.png -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:react-hooks/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 12, 14 | "sourceType": "module", 15 | "ecmaFeatures": { 16 | "jsx": true 17 | } 18 | }, 19 | "plugins": ["react", "react-hooks"], 20 | "rules": { 21 | "react/prop-types": "off", 22 | "react/no-unescaped-entities": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | ## Front-End for URL Shortener -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 20 | 21 | SnapURL 22 | 23 | 24 | 25 | 32 | 33 | 34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snapurl-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "SnapURL Team", 6 | "dependencies": { 7 | "@emotion/react": "^11.13.3", 8 | "@emotion/styled": "^11.13.0", 9 | "@fortawesome/free-brands-svg-icons": "^6.4.2", 10 | "@fortawesome/react-fontawesome": "^0.2.0", 11 | "@mui/icons-material": "^5.14.12", 12 | "@mui/material": "^5.16.7", 13 | "@testing-library/jest-dom": "^5.16.5", 14 | "@testing-library/react": "^13.4.0", 15 | "@testing-library/user-event": "^13.5.0", 16 | "axios": "^1.5.1", 17 | "file-saver": "^2.0.5", 18 | "formik": "^2.4.5", 19 | "lottie-react": "^2.4.0", 20 | "prettier": "^2.8.3", 21 | "prop-types": "^15.8.1", 22 | "qrcode.react": "^3.1.0", 23 | "react": "^18.2.0", 24 | "react-bootstrap": "^2.7.0", 25 | "react-copy-to-clipboard": "^5.1.0", 26 | "react-dom": "^18.2.0", 27 | "react-icons": "^4.7.1", 28 | "react-router-bootstrap": "^0.26.2", 29 | "react-router-dom": "^6.7.0", 30 | "react-toastify": "^9.1.3", 31 | "shortid": "^2.2.16", 32 | "web-vitals": "^2.1.4", 33 | "yup": "^1.3.2" 34 | }, 35 | "scripts": { 36 | "dev": "vite", 37 | "start": "vite dev", 38 | "build": "vite build", 39 | "test": "vite test", 40 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 41 | "preview": "vite preview", 42 | "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "@vitejs/plugin-react-swc": "^3.4.0", 64 | "eslint": "^8.52.0", 65 | "eslint-plugin-react": "^7.33.2", 66 | "eslint-plugin-react-hooks": "^4.6.0", 67 | "vite": "^4.5.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/public/logo.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SnapURL", 3 | "name": "URL Shortener App by SnapURL Team", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | } 4 | 5 | .card { 6 | box-shadow: 0px 0px 10px #8c7a7a; 7 | } 8 | 9 | .card-header { 10 | background-color: #007bff; 11 | color: white; 12 | } 13 | 14 | .card-footer a { 15 | color: #007bff; 16 | } 17 | 18 | .card-footer a:hover { 19 | color: #0056b3; 20 | text-decoration: none; 21 | } 22 | 23 | .btn-info { 24 | background-color: #007bff; 25 | border-color: #007bff; 26 | color: whitesmoke; 27 | } 28 | 29 | .btn-info:hover { 30 | background-color: #0056b3; 31 | border-color: #0056b3; 32 | color: whitesmoke; 33 | } 34 | 35 | .jumbotron { 36 | background-color: #007bff; 37 | color: white; 38 | } 39 | 40 | .jumbotron h1, 41 | .jumbotron p { 42 | text-align: center; 43 | } 44 | 45 | .form-control { 46 | background-color: #f8f9fa; 47 | border-radius: 0; 48 | } 49 | 50 | .form-control:focus { 51 | box-shadow: 0px 0px 10px #8c7a7a; 52 | } 53 | 54 | /* footer, make it always stick to bottom of the screen */ 55 | footer { 56 | position: fixed; 57 | bottom: 0; 58 | width: 100%; 59 | height: 60px; 60 | background-color: #f5f5f5; 61 | } 62 | a:hover { 63 | color: blue; 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import MyNavbar from './components/Navbar'; 3 | import LandingPage from './components/LandingPage/LandingPage'; 4 | import About from './components/About'; 5 | import Login from './components/Login'; 6 | import Signup from './components/Signup'; 7 | import Logout from './components/Logout'; 8 | // import Footer from "./components/Footer"; 9 | import History from './components/History'; 10 | import './App.css'; 11 | import UserContext from './context/UserContext'; 12 | import Contributors from './components/Contributors'; 13 | import React, { useState } from 'react'; 14 | import Linkinbio from './components/Linkinbio'; 15 | import ProfilePage from './components/ProfilePage'; 16 | import ResetPassword from './components/ResetPassword'; 17 | // import LandingPage from "./components/LandingPage/LandingPage"; 18 | // for react-toastify 19 | import 'react-toastify/dist/ReactToastify.css'; 20 | import { ToastContainer } from 'react-toastify'; 21 | import SharePage from './components/Sharepage'; 22 | import ForgotPassword from './components/ForgotPassword'; 23 | import FeedbackForm from './components/FeedbackForm'; 24 | 25 | function App() { 26 | const [user, setUser] = useState(localStorage.getItem('token') || null); 27 | 28 | return ( 29 |
30 | 31 | 32 | 33 | 34 | 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | } /> 40 | } /> 41 | } /> 42 | } /> 43 | } />{' '} 44 | {/* Use /linkinbio/* */} 45 | } />{' '} 46 | {/* Use /linkinbio/* */} 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | 52 | {/*
56 | ); 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /frontend/src/assets/images/feedback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DhananjayThomble/URL-Shortener-App/172540452aee8df56ba3e547c2d816f7e089d359/frontend/src/assets/images/feedback.png -------------------------------------------------------------------------------- /frontend/src/components/About.jsx: -------------------------------------------------------------------------------- 1 | import Accordion from 'react-bootstrap/Accordion'; 2 | 3 | import React from 'react'; 4 | 5 | function About() { 6 | return ( 7 |
8 |
9 |

About SnapURL

10 |

11 | SnapURL is an open-source URL shortener that makes converting long, 12 | cumbersome URLs into short, easy-to-share links simple. It provides 13 | user-friendly features as well as tracking capabilities to improve 14 | your web experience. 15 |

16 |

Why Use SnapURL?

17 |

18 | SnapURL is an open-source URL shortener that allows you to create, 19 | track, and share short links for free. It is a self-hosted alternative 20 | to paid URL shorteners such as Bitly and TinyURL. SnapURL is simple to 21 | use and has a clean, modern interface. It's built with React, Node, 22 | Express, and MongoDB. 23 |

24 |

Features

25 |

26 | SnapURL provides a variety of features to improve your web experience. 27 | These include: 28 |

29 |
    30 |
  1. Custom URLs
  2. 31 |
  3. Link Password Protection
  4. 32 |
  5. Link Analytics
  6. 33 |
  7. QR Code Generation
  8. 34 |
  9. Link Deletion
  10. 35 |
  11. Export generated URL's to excel file
  12. 36 |
37 |
38 |

Frequently Asked Questions

39 |
40 | 41 | 42 | 1. What is SnapURL? 43 | 44 | SnapURL is an open-source URL shortener that makes converting 45 | long, cumbersome URLs into short, easy-to-share links simple. It 46 | provides user-friendly features as well as tracking capabilities 47 | to improve your web experience. 48 | 49 | 50 | 51 | 52 | 2. How do I get started with SnapURL? 53 | 54 | 55 | Simply create a SnapURL account to get started. After registering, 56 | you can begin shortening URLs and taking advantage of our 57 | services. 58 | 59 | 60 | 61 | 3. Is SnapURL free to use? 62 | 63 | SnapURL is, indeed, completely free to use. We do not charge any 64 | fees for our services. 65 | 66 | 67 | 68 | 69 | 4. Is my data secure with SnapURL? 70 | 71 | 72 | Yes, the security of your data is a top priority for us. For added 73 | security, we use strong password hashing with Bcrypt and provide 74 | email verification and password reset via email. 75 | 76 | 77 | 78 |
79 |
80 | ); 81 | } 82 | 83 | export default About; 84 | -------------------------------------------------------------------------------- /frontend/src/components/AxiosFetch.jsx: -------------------------------------------------------------------------------- 1 | // import { set } from "mongoose"; 2 | import { useState, useEffect } from 'react'; 3 | import axios from 'axios'; 4 | 5 | const AxiosFetch = (url) => { 6 | const [data, setData] = useState(null); 7 | const [isPending, setIsPending] = useState(true); 8 | const [error, setError] = useState(null); 9 | // 'http://localhost:8000/blogs' 10 | useEffect(() => { 11 | const abortCont = new AbortController(); 12 | axios 13 | .get(url) 14 | .then((data) => { 15 | // console.log("axios fetch",data.data); 16 | setData(data.data); 17 | setIsPending(false); 18 | setError(null); 19 | }) 20 | .catch((err) => { 21 | if (err.name === 'AbortError') { 22 | console.log('fetch aborted'); 23 | } else { 24 | setIsPending(false); 25 | setError(err.message); 26 | } 27 | }); 28 | return () => abortCont.abort(); 29 | }, [url]); //empty dependency array prevents the useEffect from running for every change 30 | // console.log('axios',data); 31 | return { data, isPending, error }; 32 | }; 33 | 34 | export default AxiosFetch; 35 | -------------------------------------------------------------------------------- /frontend/src/components/Contributers.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | .contributers { 3 | font-family: 'poppins'; 4 | } 5 | 6 | .contributers-main { 7 | display: flex; 8 | align-items: center; 9 | padding: 0px 0px; 10 | width: 80vw; 11 | margin: auto; 12 | justify-content: space-around; 13 | } 14 | .contributers-main .contributers-animation { 15 | width: 100%; 16 | min-width: 300px; 17 | flex: 1; 18 | } 19 | .contributers .contributrers-main-text { 20 | flex: 1; 21 | } 22 | .contributers .contributrers-main-text h1 { 23 | text-align: left; 24 | font-size: 60px; 25 | font-weight: 800; 26 | } 27 | .contributers .contributrers-main-text h2 { 28 | font-size: 20px; 29 | font-weight: 500; 30 | } 31 | .contributers .contributrers-main-text h3 { 32 | font-size: 15px; 33 | color: grey; 34 | font-weight: 400; 35 | } 36 | 37 | .contributers .title { 38 | font-size: 50px; 39 | font-weight: 800; 40 | width: 90vw; 41 | margin: auto; 42 | display: flex; 43 | align-items: center; 44 | } 45 | 46 | .contributers .title .circle { 47 | font-size: 30px; 48 | margin-right: 20px; 49 | color: rgb(75, 63, 107); 50 | } 51 | 52 | .contributers .contributer-container { 53 | width: 90vw; 54 | margin: auto; 55 | margin-top: 40px; 56 | display: grid; 57 | grid-template-columns: auto auto auto auto; 58 | grid-auto-flow: row; 59 | gap: 20px; 60 | } 61 | 62 | .profile { 63 | display: flex; 64 | flex-direction: column; 65 | align-items: center; 66 | padding: 10px; 67 | border-radius: 20px; 68 | } 69 | 70 | .profile:hover { 71 | cursor: pointer; 72 | background-color: rgba(75, 63, 107, 0.235); 73 | } 74 | 75 | .profile img { 76 | height: auto; 77 | width: 10rem; 78 | border-radius: 50%; 79 | } 80 | 81 | .profile h1 { 82 | font-size: 1rem; 83 | margin-top: 20px; 84 | color: black; 85 | text-align: center; 86 | } 87 | 88 | .loading { 89 | width: 50px; 90 | height: 50px; 91 | margin: 20px auto; 92 | border: 4px solid #f3f3f3; 93 | border-top: 4px solid rgb(75, 63, 107); 94 | border-radius: 50%; 95 | animation: spin 1.5s linear infinite; 96 | } 97 | 98 | @keyframes spin { 99 | 0% { 100 | transform: rotate(0deg); 101 | } 102 | 103 | 100% { 104 | transform: rotate(360deg); 105 | } 106 | } 107 | 108 | @media screen and (max-width: 800px) { 109 | .contributers-main { 110 | /* background-color: #ff5722; */ 111 | flex-direction: column-reverse; 112 | justify-content: center; 113 | width: 90vw; 114 | } 115 | 116 | .contributers-main .contributers-animation { 117 | min-width: 300px; 118 | width: 50%; 119 | /* background-color: red; */ 120 | } 121 | .contributers-main .contributrers-main-text { 122 | padding-top: 40px; 123 | text-align: center; 124 | } 125 | .contributers-main .contributrers-main-text h1 { 126 | font-size: 30px; 127 | text-align: center; 128 | } 129 | .contributers .title { 130 | font-size: 25px; 131 | margin-right: 10px; 132 | } 133 | .contributers .title .circle { 134 | font-size: 20px; 135 | margin-right: 10px; 136 | color: rgb(75, 63, 107); 137 | } 138 | .contributers .contributer-container { 139 | /* background-color: red; */ 140 | width: 100vw; 141 | max-width: 99vw; 142 | margin: auto; 143 | margin-top: 40px; 144 | display: grid; 145 | grid-template-columns: auto auto; 146 | grid-auto-flow: row; 147 | gap: 0px; 148 | overflow: hidden; 149 | padding: 0px 10px; 150 | } 151 | .profile img { 152 | width: 7rem; 153 | } 154 | .profile h1 { 155 | font-size: 0.8rem; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /frontend/src/components/Contributers.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AxiosFetch from './AxiosFetch'; 3 | import Lottie from 'lottie-react'; 4 | import './Contributers.css'; 5 | 6 | import Circle from '@mui/icons-material/Circle'; 7 | import cont1 from '../assets/cont1.json'; 8 | 9 | function Contributers() { 10 | const { data, isPending, error } = AxiosFetch( 11 | 'https://api.github.com/repos/DhananjayThomble/URL-Shortener-App/contributors', 12 | ); 13 | return ( 14 |
15 |
16 | 21 |
22 |

Github Contributers,

23 |

24 | Collaboration in Code: Where Ideas Converge and Innovations Thrive 25 | 🚀 26 |

27 |

#OpenSource #CodeUnity

28 |
29 |
30 | 31 |

32 | 33 | The Contributers 34 |

35 | 36 | {error &&
{error}
} 37 | {isPending &&
} 38 |
39 | {data && 40 | data.map((item) => ( 41 | 47 |
48 | 49 |

{item.login}

50 |
51 |
52 | ))} 53 |
54 |
55 | ); 56 | } 57 | 58 | export default Contributers; 59 | -------------------------------------------------------------------------------- /frontend/src/components/Contributors.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | .contributors { 3 | font-family: 'poppins'; 4 | } 5 | 6 | .contributors-main { 7 | display: flex; 8 | align-items: center; 9 | padding: 0px 0px; 10 | width: 80vw; 11 | margin: auto; 12 | justify-content: space-around; 13 | } 14 | .contributors-main .contributors-animation { 15 | width: 100%; 16 | min-width: 300px; 17 | flex: 1; 18 | } 19 | .contributors .contributrers-main-text { 20 | flex: 1; 21 | } 22 | .contributors .contributrers-main-text h1 { 23 | text-align: left; 24 | font-size: 60px; 25 | font-weight: 800; 26 | } 27 | .contributors .contributrers-main-text h2 { 28 | font-size: 20px; 29 | font-weight: 500; 30 | } 31 | .contributors .contributrers-main-text h3 { 32 | font-size: 15px; 33 | color: grey; 34 | font-weight: 400; 35 | } 36 | 37 | .contributors .title { 38 | font-size: 50px; 39 | font-weight: 800; 40 | width: 90vw; 41 | margin: auto; 42 | display: flex; 43 | align-items: center; 44 | } 45 | 46 | .contributors .title .circle { 47 | font-size: 30px; 48 | margin-right: 20px; 49 | color: rgb(75, 63, 107); 50 | } 51 | 52 | .contributors .contributer-container { 53 | width: 90vw; 54 | margin: auto; 55 | margin-top: 40px; 56 | display: grid; 57 | grid-template-columns: auto auto auto auto; 58 | grid-auto-flow: row; 59 | gap: 20px; 60 | } 61 | 62 | .profile { 63 | display: flex; 64 | flex-direction: column; 65 | align-items: center; 66 | padding: 10px; 67 | border-radius: 20px; 68 | } 69 | 70 | .profile:hover { 71 | cursor: pointer; 72 | background-color: rgba(75, 63, 107, 0.235); 73 | } 74 | 75 | .profile img { 76 | height: auto; 77 | width: 10rem; 78 | border-radius: 50%; 79 | } 80 | 81 | .profile h1 { 82 | font-size: 1rem; 83 | margin-top: 20px; 84 | color: black; 85 | text-align: center; 86 | } 87 | 88 | .loading { 89 | width: 50px; 90 | height: 50px; 91 | margin: 20px auto; 92 | border: 4px solid #f3f3f3; 93 | border-top: 4px solid rgb(75, 63, 107); 94 | border-radius: 50%; 95 | animation: spin 1.5s linear infinite; 96 | } 97 | 98 | @keyframes spin { 99 | 0% { 100 | transform: rotate(0deg); 101 | } 102 | 103 | 100% { 104 | transform: rotate(360deg); 105 | } 106 | } 107 | 108 | @media screen and (max-width: 800px) { 109 | .contributors-main { 110 | /* background-color: #ff5722; */ 111 | flex-direction: column-reverse; 112 | justify-content: center; 113 | width: 90vw; 114 | } 115 | 116 | .contributors-main .contributors-animation { 117 | min-width: 300px; 118 | width: 50%; 119 | /* background-color: red; */ 120 | } 121 | .contributors-main .contributrers-main-text { 122 | padding-top: 40px; 123 | text-align: center; 124 | } 125 | .contributors-main .contributrers-main-text h1 { 126 | font-size: 30px; 127 | text-align: center; 128 | } 129 | .contributors .title { 130 | font-size: 25px; 131 | margin-right: 10px; 132 | } 133 | .contributors .title .circle { 134 | font-size: 20px; 135 | margin-right: 10px; 136 | color: rgb(75, 63, 107); 137 | } 138 | .contributors .contributer-container { 139 | /* background-color: red; */ 140 | width: 100vw; 141 | max-width: 99vw; 142 | margin: auto; 143 | margin-top: 40px; 144 | display: grid; 145 | grid-template-columns: auto auto; 146 | grid-auto-flow: row; 147 | gap: 0px; 148 | overflow: hidden; 149 | padding: 0px 10px; 150 | } 151 | .profile img { 152 | width: 7rem; 153 | } 154 | .profile h1 { 155 | font-size: 0.8rem; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /frontend/src/components/Contributors.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AxiosFetch from './AxiosFetch'; 3 | import Lottie from 'lottie-react'; 4 | import './Contributors.css'; 5 | 6 | import Circle from '@mui/icons-material/Circle'; 7 | import cont1 from '../assets/cont1.json'; 8 | 9 | function Contributors() { 10 | const { data, isPending, error } = AxiosFetch( 11 | 'https://api.github.com/repos/DhananjayThomble/URL-Shortener-App/contributors', 12 | ); 13 | return ( 14 |
15 |
16 | 21 |
22 |

Github Contributors,

23 |

24 | Collaboration in Code: Where Ideas Converge and Innovations Thrive 25 | 🚀 26 |

27 |

#OpenSource #CodeUnity

28 |
29 |
30 | 31 |

32 | 33 | The Contributors 34 |

35 | 36 | {error &&
{error}
} 37 | {isPending &&
} 38 |
39 | {data && 40 | data.map((item) => ( 41 | 47 |
48 | 49 |

{item.login}

50 |
51 |
52 | ))} 53 |
54 |
55 | ); 56 | } 57 | 58 | export default Contributors; 59 | -------------------------------------------------------------------------------- /frontend/src/components/ExportToExcel.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, Col, Container } from 'react-bootstrap'; 3 | import axios from 'axios'; 4 | import { saveAs } from 'file-saver'; 5 | import { toast } from 'react-toastify'; 6 | 7 | const ExportToExcel = () => { 8 | const [isLoading, setIsLoading] = useState(false); 9 | 10 | const handleExport = async () => { 11 | setIsLoading(true); 12 | toast.info('Exporting to Excel...'); 13 | try { 14 | // Make a request to the backend to fetch the Excel file 15 | const URL = `${import.meta.env.VITE_API_ENDPOINT}/api/export`; 16 | const response = await axios.get(URL, { 17 | responseType: 'blob', 18 | headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }, 19 | }); 20 | 21 | // Save the Excel file using FileSaver.js 22 | saveAs(response.data, 'Generated_URLs.xlsx'); 23 | toast.success('Exported to Excel'); 24 | setIsLoading(false); 25 | } catch (error) { 26 | console.error(error); 27 | toast.error('Error exporting to Excel'); 28 | } 29 | }; 30 | 31 | return ( 32 | 33 | 34 |
35 | 43 |
44 | 45 |
46 | ); 47 | }; 48 | 49 | export default ExportToExcel; 50 | -------------------------------------------------------------------------------- /frontend/src/components/FeedbackForm.css: -------------------------------------------------------------------------------- 1 | .feedback__form__container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .feedback__form { 8 | display: flex; 9 | flex-direction: row; 10 | justify-content: space-between; 11 | align-items: center; 12 | padding: 25px; 13 | margin: 30px 0px; 14 | background-color: rgb(230, 230, 250); 15 | border-radius: 25px; 16 | width: 80%; 17 | box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, 18 | rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, 19 | rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset; 20 | } 21 | 22 | .feedback__form__img { 23 | width: 40%; 24 | } 25 | 26 | .feedback__img { 27 | width: 90%; 28 | } 29 | 30 | .feedback__form__details { 31 | width: 60%; 32 | padding: 0px 10px; 33 | } 34 | 35 | .feedback__form__details * { 36 | margin: 5px; 37 | } 38 | 39 | .feedback__form__details input { 40 | background: 0; 41 | border: 0; 42 | outline: none; 43 | font-size: 18px; 44 | transition: padding 0.3s 0.2s ease; 45 | } 46 | 47 | .feedback__form__details input:focus { 48 | padding-bottom: 5px; 49 | } 50 | 51 | .feedback__form__details input:focus + .line:after { 52 | transform: scaleX(1); 53 | } 54 | 55 | .feedback__form__details .field { 56 | position: relative; 57 | } 58 | 59 | .feedback__form__details .line { 60 | width: 100%; 61 | height: 3px; 62 | position: absolute; 63 | bottom: -8px; 64 | background: #bdc3c7; 65 | } 66 | 67 | .feedback__form__details .line:after { 68 | content: ' '; 69 | position: absolute; 70 | float: right; 71 | width: 100%; 72 | height: 3px; 73 | transform: scalex(0); 74 | transition: transform 0.3s ease; 75 | background: rgb(75, 63, 107); 76 | } 77 | 78 | .feedback__form__details textarea { 79 | padding: 10px; 80 | border-radius: 10px; 81 | height: 300px; 82 | width: 100%; 83 | resize: none; 84 | overflow: auto; 85 | border: none; 86 | outline: none; 87 | } 88 | 89 | .error__message { 90 | color: red; 91 | font-weight: bold; 92 | margin: 0; 93 | padding: 0; 94 | } 95 | 96 | .feedback__form__details textarea:focus { 97 | border: 3px solid rgb(220, 189, 221); 98 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 99 | } 100 | 101 | .form__submit__btn { 102 | text-align: center; 103 | } 104 | 105 | .feedback__submit__btn { 106 | background-color: #ffffff; 107 | border-radius: 4px; 108 | box-shadow: rgba(45, 35, 66, 0.4) 0 2px 4px, 109 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; 110 | color: #36395a; 111 | cursor: pointer; 112 | height: 48px; 113 | padding: 0px 16px; 114 | transition: box-shadow 0.15s, transform 0.15s; 115 | font-size: 18px; 116 | border: none; 117 | } 118 | 119 | .feedback__submit__btn:focus { 120 | box-shadow: #d6d6e7 0 0 0 1.5px inset, rgba(45, 35, 66, 0.4) 0 2px 4px, 121 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; 122 | } 123 | 124 | .feedback__submit__btn:hover { 125 | box-shadow: rgba(45, 35, 66, 0.4) 0 4px 8px, 126 | rgba(45, 35, 66, 0.3) 0 7px 13px -3px, #d6d6e7 0 -3px 0 inset; 127 | transform: translateY(-2px); 128 | } 129 | 130 | .feedback__submit__btn:active { 131 | box-shadow: #d6d6e7 0 3px 7px inset; 132 | transform: translateY(2px); 133 | } 134 | 135 | @media screen and (max-width: 768px) { 136 | .feedback__form { 137 | flex-direction: column; 138 | } 139 | .feedback__form__img, 140 | .feedback__form__details { 141 | width: 100%; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /frontend/src/components/FeedbackForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './FeedbackForm.css'; 3 | import Image from '../assets/images/feedback.png'; 4 | import { toast } from 'react-toastify'; 5 | import axios from 'axios'; 6 | 7 | const FeedbackForm = () => { 8 | const [name, setName] = useState(''); 9 | const [email, setEmail] = useState(''); 10 | const [attachment, setAttachment] = useState(null); 11 | // TODO: Implement attachments later 12 | const [feedback, setFeedback] = useState(''); 13 | const [nameError, setNameError] = useState(''); 14 | const [emailError, setEmailError] = useState(''); 15 | const [feedbackError, setFeedbackError] = useState(''); 16 | const [fileInputKey, setFileInputKey] = useState(0); 17 | 18 | const handleAttachmentChange = (event) => { 19 | setAttachment(event.target.files[0]); 20 | }; 21 | 22 | const resetState = () => { 23 | setName(''); 24 | setEmail(''); 25 | setFeedback(''); 26 | setAttachment(null); 27 | setFileInputKey((prevKey) => prevKey + 1); 28 | }; 29 | 30 | const isNameValid = (input) => { 31 | const namePattern = /^[a-zA-Z\s-]*$/; 32 | return namePattern.test(input); 33 | }; 34 | 35 | const isEmailValid = (input) => { 36 | const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; 37 | return emailPattern.test(input); 38 | }; 39 | 40 | const handleSubmit = async () => { 41 | let isFormValid = true; 42 | setEmailError(''); 43 | setNameError(''); 44 | setFeedbackError(''); 45 | 46 | if (!isNameValid(name) || !name) { 47 | setNameError('Please enter a valid name.'); 48 | isFormValid = false; 49 | } 50 | 51 | if (!isEmailValid(email) || !email) { 52 | setEmailError('Please enter a valid email address.'); 53 | isFormValid = false; 54 | } 55 | 56 | if (!feedback) { 57 | setFeedbackError('Please enter your feedback'); 58 | isFormValid = false; 59 | } 60 | 61 | if (isFormValid) { 62 | // Submit form details to the API 63 | const apiEndpoint = `${import.meta.env.VITE_API_ENDPOINT}/auth/feedback`; 64 | 65 | try { 66 | const response = await axios.post( 67 | apiEndpoint, 68 | { 69 | message: feedback, 70 | rating: 5, 71 | }, 72 | { 73 | headers: { 74 | Authorization: `Bearer ${localStorage.getItem('token')}`, 75 | }, 76 | }, 77 | ); 78 | console.log(response.status); 79 | 80 | toast.success('Submitted'); 81 | resetState(); 82 | } catch (error) { 83 | if (error.response.status === 400) { 84 | const { error: errorMsg } = error.response.data; 85 | console.log(error, errorMsg); 86 | 87 | toast.error(errorMsg); 88 | } else if ( 89 | error.response.status === 404 || 90 | error.response.status === 401 91 | ) { 92 | toast.error('Please log in or sign up first!'); 93 | } else { 94 | toast.error('Something went wrong. Please try again later.'); 95 | } 96 | } 97 | } 98 | }; 99 | 100 | return ( 101 |
102 |
103 |
104 | Feedback 105 |
106 |
107 |
108 | setName(e.target.value)} 113 | /> 114 |
115 |
116 | {nameError &&

{nameError}

} 117 |
118 | setEmail(e.target.value)} 123 | /> 124 |
125 |
126 | {emailError &&

{emailError}

} 127 |
128 | handleAttachmentChange(e)} 133 | /> 134 |
135 |
136 | 140 |