├── server
├── .gitignore
├── routes
│ ├── verify.js
│ ├── contact.js
│ ├── digest.js
│ ├── image.js
│ └── users.js
├── models
│ ├── digest.js
│ ├── contact_format.js
│ ├── user_attempts.js
│ └── user.js
├── util
│ ├── nodemailer.js
│ └── util.js
├── controllers
│ ├── contact_controller.js
│ ├── image_getbyuser.js
│ ├── digest_controller.js
│ ├── validation.js
│ ├── image_search.js
│ ├── verify_token.js
│ ├── users_signup.js
│ └── users_login.js
├── package.json
├── views
│ └── unblocked.html
├── static
│ └── message.js
├── README.md
├── index.js
└── swagger.json
├── client
├── public
│ ├── robots.txt
│ ├── static
│ │ └── img
│ │ │ └── signup.png
│ ├── manifest.json
│ ├── index.html
│ └── index.css
├── src
│ ├── index.css
│ ├── static
│ │ ├── img
│ │ │ └── signup.png
│ │ ├── config.js
│ │ └── icons_data.js
│ ├── util
│ │ ├── config.js
│ │ ├── util.js
│ │ ├── toast.js
│ │ └── validation.js
│ ├── components
│ │ ├── Items
│ │ │ ├── PasswordIcon.js
│ │ │ ├── BlockedBox.js
│ │ │ ├── AttackBlock.js
│ │ │ └── Loader.js
│ │ ├── Digest.js
│ │ ├── Slider.js
│ │ ├── Navbar.js
│ │ ├── Footer.js
│ │ ├── Contact.js
│ │ ├── Landing.js
│ │ ├── Login.js
│ │ └── Signup.js
│ ├── index.js
│ └── App.js
├── tailwind.config.js
├── README.md
└── package.json
├── img
├── login_1.png
├── login_2.png
├── auth_fail.png
├── auth_success.png
├── landing_page.png
├── account_blocked.png
├── registration_1.png
├── registration_2.png
├── account_unblocked.png
└── notification_email.png
├── LICENSE
├── SCREENSHOTS.md
└── README.md
/server/.gitignore:
--------------------------------------------------------------------------------
1 | .env
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/img/login_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/login_1.png
--------------------------------------------------------------------------------
/img/login_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/login_2.png
--------------------------------------------------------------------------------
/img/auth_fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/auth_fail.png
--------------------------------------------------------------------------------
/img/auth_success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/auth_success.png
--------------------------------------------------------------------------------
/img/landing_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/landing_page.png
--------------------------------------------------------------------------------
/img/account_blocked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/account_blocked.png
--------------------------------------------------------------------------------
/img/registration_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/registration_1.png
--------------------------------------------------------------------------------
/img/registration_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/registration_2.png
--------------------------------------------------------------------------------
/img/account_unblocked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/account_unblocked.png
--------------------------------------------------------------------------------
/img/notification_email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/img/notification_email.png
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background-color: #2B2B2B;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/static/img/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/client/src/static/img/signup.png
--------------------------------------------------------------------------------
/client/public/static/img/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/prathamesh-a/graphical-password-authentication/HEAD/client/public/static/img/signup.png
--------------------------------------------------------------------------------
/client/src/static/config.js:
--------------------------------------------------------------------------------
1 | export const api = {
2 | //url: "http://localhost:5000"
3 | //url:"https://crazy-fish-tights.cyclic.app"
4 | url: "https://graphical-auth-server.onrender.com"
5 | }
--------------------------------------------------------------------------------
/server/routes/verify.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { verify } from '../controllers/verify_token.js'
3 |
4 | const router = express.Router()
5 |
6 | router.get('/', verify)
7 |
8 | export { router as VerifyRoute }
--------------------------------------------------------------------------------
/server/routes/contact.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import { contactController } from "../controllers/contact_controller.js"
3 |
4 | const router = express.Router()
5 |
6 | router.post('/', contactController)
7 |
8 | export { router }
--------------------------------------------------------------------------------
/server/routes/digest.js:
--------------------------------------------------------------------------------
1 | import express from "express"
2 | import { digestController } from "../controllers/digest_controller.js"
3 |
4 | const router = express.Router()
5 |
6 | router.post('/', digestController)
7 |
8 | export {router as DigestRoutes}
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | ],
6 | "start_url": ".",
7 | "display": "standalone",
8 | "theme_color": "#000000",
9 | "background_color": "#ffffff"
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/util/config.js:
--------------------------------------------------------------------------------
1 | export const Page = {
2 | HOME_PAGE: "home",
3 | SIGNUP_PAGE: "signup",
4 | ABOUT: "about",
5 | CONTACT: "contact",
6 | LOGIN_PAGE: "loginpage"
7 | }
8 |
9 | export const header = {headers: {"Access-Control-Allow-Origin": "*"}}
--------------------------------------------------------------------------------
/server/routes/image.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { getByUser } from '../controllers/image_getbyuser.js'
3 | import { search as imageSearch } from '../controllers/image_search.js'
4 |
5 | const router = express.Router()
6 |
7 | router.get('/search', imageSearch)
8 | router.get('/', getByUser)
9 |
10 | export { router }
--------------------------------------------------------------------------------
/server/models/digest.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongooseUniqueValidator from "mongoose-unique-validator";
3 |
4 | const Schema = mongoose.Schema
5 |
6 | const digestSchema = new Schema({
7 | email: {type: String, required: true, unique: true}
8 | })
9 |
10 | digestSchema.plugin(mongooseUniqueValidator)
11 |
12 | const digestModel = mongoose.model('Digest', digestSchema)
13 |
14 | export {digestModel}
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import { loginController } from '../controllers/users_login.js'
3 | import { signupController } from '../controllers/users_signup.js'
4 | import { check } from '../controllers/validation.js'
5 |
6 | const router = express.Router()
7 |
8 | router.post('/signup', signupController)
9 | router.post('/login', loginController)
10 |
11 | router.get('/check', check)
12 |
13 | export { router }
--------------------------------------------------------------------------------
/client/src/components/Items/PasswordIcon.js:
--------------------------------------------------------------------------------
1 | export default function PasswordIcon(props) {
2 |
3 | const selectedClasses = "bg-purple-500 shadow-xl"
4 |
5 | return (
6 | props.onClick(props.id, props.iteration)}
10 | className={`w-[80%] transition duration-500 ease-in-out hover:shadow-2xl hover:scale-105 rounded-lg p-1 cursor-pointer ${props.selected ? selectedClasses : ""}`}/>
11 | )
12 | }
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{html,js}"],
4 | theme: {
5 | extend: {},
6 | screens: {
7 | xs: "480px",
8 | ss: "620px",
9 | sm: "768px",
10 | md: "1024px",
11 | lg: "1200px",
12 | xl: "1700px",
13 | },
14 | // colors: {
15 | // 'purple': '#A259FF',
16 | // 'darkgray': '#2B2B2B',
17 | // 'mdgray': '#3B3B3B',
18 | // }
19 | },
20 | plugins: [],
21 | }
22 |
--------------------------------------------------------------------------------
/server/models/contact_format.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongooseUniqueValidator from "mongoose-unique-validator";
3 |
4 | const Schema = mongoose.Schema
5 |
6 | const contactFormat = new Schema({
7 | name: {type: String, required: true},
8 | email: {type: String, required: true},
9 | message: {type: String, required: true}
10 | })
11 |
12 | contactFormat.plugin(mongooseUniqueValidator)
13 |
14 | const contactFormatModel = mongoose.model('Message', contactFormat)
15 |
16 | export { contactFormatModel }
--------------------------------------------------------------------------------
/server/util/nodemailer.js:
--------------------------------------------------------------------------------
1 | //import * as dotenv from 'dotenv'
2 | import { createTransport } from "nodemailer"
3 |
4 | //dotenv.config()
5 |
6 | const transporter = createTransport({
7 | service: "gmail",
8 | auth: {
9 | user: process.env.SMPT_USER,
10 | pass: process.env.SMTP_PASSWORD
11 | }
12 | })
13 |
14 | // const mailOptions = {
15 | // from: "graphicalpassauth@gmail.com",
16 | // to: "autipratham1671@gmail.com",
17 | // subject: "Test Email",
18 | // text: "test"
19 | // }
20 |
21 | export { transporter }
--------------------------------------------------------------------------------
/client/src/util/util.js:
--------------------------------------------------------------------------------
1 | function removeElementFromArray(element, array) {
2 | const index = array.indexOf(element)
3 | if (index > -1) array.splice(index, 1)
4 | }
5 |
6 | function getNameByNumber(num) {
7 | // eslint-disable-next-line default-case
8 | switch (num) {
9 | case 1:
10 | return "First"
11 | case 2:
12 | return "Second"
13 | case 3:
14 | return "Third"
15 | case 4:
16 | return "Fourth"
17 | }
18 | }
19 |
20 | export {removeElementFromArray, getNameByNumber}
--------------------------------------------------------------------------------
/server/models/user_attempts.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongooseUniqueValidator from "mongoose-unique-validator";
3 |
4 | const Schema = mongoose.Schema
5 |
6 | const userAttemptsSchema = new Schema({
7 | username: { type: String, required: true, unique: true },
8 | email: { type: String, required: true, unique: true },
9 | attempts: {type: Number, required: true},
10 | token: {type: String}
11 | })
12 |
13 | userAttemptsSchema.plugin(mongooseUniqueValidator)
14 |
15 | const userAttemptsModel = mongoose.model('UserAttempts', userAttemptsSchema)
16 |
17 | export { userAttemptsModel }
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import {ToastContainer} from "react-toastify";
6 | import 'react-toastify/dist/ReactToastify.min.css';
7 | import {Analytics} from "@vercel/analytics/react";
8 |
9 | const root = ReactDOM.createRoot(document.getElementById('root'));
10 |
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import mongooseUniqueValidator from "mongoose-unique-validator";
3 |
4 | const Schema = mongoose.Schema
5 |
6 | const userSchema = new Schema({
7 | username: { type: String, required: true, unique: true },
8 | email: { type: String, required: true, unique: true },
9 | password: {type: String, required: true, minlength: 8 },
10 | pattern: { type: [String], required: true },
11 | sequence: { type: Boolean, required: true },
12 | sets: {type: [[Object]], required: true}
13 | })
14 |
15 | userSchema.plugin(mongooseUniqueValidator)
16 |
17 | const usertModel = mongoose.model('User', userSchema)
18 |
19 | export { usertModel }
--------------------------------------------------------------------------------
/client/src/util/toast.js:
--------------------------------------------------------------------------------
1 | import {toast} from "react-toastify";
2 |
3 | function Toast(message) {
4 | toast.error(message, {position: "top-center", autoClose: 3000, hideProgressBar: false,
5 | closeOnClick: true,
6 | pauseOnHover: true,
7 | draggable: true,
8 | progress: undefined,
9 | theme: "dark",
10 | })
11 | }
12 |
13 | function successToast(message) {
14 | toast.success(message, {position: "top-center", autoClose: 3000, hideProgressBar: false,
15 | closeOnClick: true,
16 | pauseOnHover: true,
17 | draggable: true,
18 | progress: undefined,
19 | theme: "dark",
20 | })
21 | }
22 |
23 | export {Toast, successToast}
--------------------------------------------------------------------------------
/client/src/components/Items/BlockedBox.js:
--------------------------------------------------------------------------------
1 | import {faClose, faWarning} from "@fortawesome/free-solid-svg-icons";
2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
3 |
4 | export default function BlockedBox(props) {
5 | return (
6 |
7 |
This account has been blocked please check your email.
8 |
props.onClick(false)} className="mr-4">
9 |
10 |
11 |
12 | )
13 | }
--------------------------------------------------------------------------------
/client/src/components/Items/AttackBlock.js:
--------------------------------------------------------------------------------
1 | export default function AttackBlock(props) {
2 | return (
3 |
4 |
5 |
6 |
7 |
{props.title}
8 |
9 |
12 |
13 |
14 | )
15 | }
--------------------------------------------------------------------------------
/client/src/components/Items/Loader.js:
--------------------------------------------------------------------------------
1 | import {Triangle} from "react-loader-spinner";
2 |
3 | export default function Loader() {
4 | return (
5 |
7 |
8 |
9 |
18 |
19 |
Loading please wait...
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/client/src/util/validation.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {api} from "../static/config";
3 | import {header} from "./config";
4 |
5 | async function checkUsername(username, setLoading) {
6 | let flag
7 | setLoading(true)
8 | await axios.get(`${api.url}/api/user/check?username=${username}`, header)
9 | .then(r => {
10 | flag = r.data.exists
11 | setLoading(false)
12 | })
13 | .catch(err => console.log(err))
14 | setLoading(false)
15 | return flag
16 | }
17 |
18 | async function checkEmail(email, setLoading) {
19 | let flag
20 | setLoading(true)
21 | await axios.get(`${api.url}/api/user/check?email=${email}`)
22 | .then(r => {
23 | flag = r.data.exists
24 | setLoading(false)
25 | })
26 | .catch(err => console.log(err))
27 | setLoading(false)
28 | return flag
29 | }
30 |
31 | export { checkEmail, checkUsername }
--------------------------------------------------------------------------------
/server/controllers/contact_controller.js:
--------------------------------------------------------------------------------
1 | import { contactFormatModel } from "../models/contact_format.js"
2 | import { commons } from "../static/message.js"
3 |
4 | const contact = async (req, res, next) => {
5 |
6 | const {name, email, message} = req.body
7 |
8 | if (typeof name === 'undefined' || typeof email === 'undefined' || typeof message === 'undefined') {
9 | res.status(406).json({
10 | message: commons.invalid_params,
11 | format: "[name, email, message]"
12 | })
13 | return next()
14 | }
15 |
16 | const contactFormat = new contactFormatModel({name, email, message})
17 |
18 | try {contactFormat.save()}
19 | catch (err) {
20 | console.log(err)
21 | res.status(500).json({message: "Error saving into database."})
22 | return next()
23 | }
24 |
25 | res.status(200).json({message: "Saved"})
26 | }
27 |
28 | export {contact as contactController}
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gpa_backend",
3 | "version": "0.1.0",
4 | "description": "Backend server for the Graphical Password Authentication client.",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node index.js"
10 | },
11 | "author": "inix",
12 | "license": "ISC",
13 | "dependencies": {
14 | "bcryptjs": "^2.4.3",
15 | "body-parser": "^1.20.1",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.0.3",
18 | "express": "^4.18.2",
19 | "jsonwebtoken": "^9.0.0",
20 | "mongodb": "^4.13.0",
21 | "mongoose": "^6.9.0",
22 | "mongoose-unique-validator": "^3.1.0",
23 | "nanoid": "^4.0.1",
24 | "node-fetch": "^3.3.0",
25 | "nodemailer": "^6.9.1",
26 | "nodemon": "^2.0.20",
27 | "swagger-jsdoc": "^6.2.8",
28 | "swagger-ui-express": "^4.6.0",
29 | "unsplash-js": "^7.0.15",
30 | "uuid": "^9.0.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/controllers/image_getbyuser.js:
--------------------------------------------------------------------------------
1 | import { commons, login_messages as msg } from "../static/message.js"
2 | import { usertModel as User } from "../models/user.js"
3 |
4 | const getByUser = async (req, res, next) => {
5 |
6 | var { username } = req.query
7 | username = username.toLowerCase()
8 | let existingUser
9 |
10 | if (typeof username === 'undefined') {
11 | res.status(500).json({
12 | message: commons.invalid_params,
13 | format: "username"
14 | })
15 | return next()
16 | }
17 |
18 | try { existingUser = await User.findOne({username: username}) }
19 | catch(err) {
20 | console.log(err)
21 | res.status(401).json({message: "Error occured while fetching from DB"})
22 | return next()
23 | }
24 |
25 | if (!existingUser) {
26 | res.status(401).json({message: msg.user_not_exist})
27 | return next()
28 | }
29 |
30 | res.send(existingUser.sets)
31 | }
32 |
33 | export { getByUser }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Prathamesh Auti
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 |
--------------------------------------------------------------------------------
/server/views/unblocked.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Graphical Auth System
10 |
11 |
12 |
13 |
14 |
Graphical Password Auth
15 |
16 |
17 |
18 |
19 |
Your account is now unblocked!
20 |
21 |
22 |
23 | You can close this page.
24 |
25 |
--------------------------------------------------------------------------------
/server/static/message.js:
--------------------------------------------------------------------------------
1 | const commons = {
2 | invalid_params: 'Invalid request params, please check format below.',
3 | token_failed: 'Internal server error, try again later.'
4 | }
5 |
6 | const login_messages = {
7 | format: ["username", "password", "pattern" ],
8 | db_user_failed: 'Error occured while logging in, please try again later.',
9 | user_not_exist: 'User does not exists.',
10 | db_pass_failed: 'Error occured while logging in, please try again later.',
11 | invalid_credentials: 'Invalid credentials given.',
12 | success: 'Logged in successfully.'
13 | }
14 |
15 | const signup_messages = {
16 | format: ["username", "email", "password", "pattern", "sets", "sequence"],
17 | db_user_failed: 'Error occured finding user on DB, please try again later.',
18 | user_already_exist: 'User already exists.',
19 | pass_hash_err: 'Error occured while hashing passwprd, please try again later.',
20 | db_save_err: 'Error occured while saving into db, please try again later.',
21 | }
22 |
23 | const validation_messages = {
24 | search_err: 'Error occured while searching, please try again later.'
25 | }
26 |
27 | export { login_messages, signup_messages, commons, validation_messages }
28 |
--------------------------------------------------------------------------------
/server/controllers/digest_controller.js:
--------------------------------------------------------------------------------
1 | import { digestModel } from "../models/digest.js"
2 | import { commons } from "../static/message.js"
3 |
4 | const digest = async (req, res, next) => {
5 | var currentEmail
6 | var newEmail
7 | const { email } = req.body
8 |
9 | if (typeof email === 'undefined') {
10 | res.status(406).json({
11 | message: commons.invalid_params,
12 | format: "email"
13 | })
14 | return next()
15 | }
16 |
17 | try {currentEmail = await digestModel.findOne({email: email})}
18 | catch(err) {
19 | console.log(err)
20 | res.status(500).json({message: "Error occured, try again later."})
21 | return next()
22 | }
23 |
24 | if (currentEmail) {
25 | res.status(500).json({message: "Already subscribed."})
26 | return next()
27 | }
28 |
29 | newEmail = new digestModel({email})
30 |
31 | try{newEmail.save()}
32 | catch(err) {
33 | console.log(err)
34 | res.status(500).json({message: "Error occured, try again later."})
35 | return next()
36 | }
37 |
38 | res.status(200).json({message: "Subscribed"})
39 |
40 | }
41 |
42 | export {digest as digestController}
--------------------------------------------------------------------------------
/server/controllers/validation.js:
--------------------------------------------------------------------------------
1 | import { usertModel as User } from '../models/user.js'
2 | import { commons, validation_messages as msg } from '../static/message.js';
3 |
4 | const check = async (req, res, next) => {
5 | let user;
6 | var {username, email} = req.query
7 |
8 | if (typeof username === 'undefined' && typeof email === 'undefined') {
9 | res.status(500).json({
10 | message: commons.invalid_params,
11 | format: "username or email"
12 | })
13 | return next()
14 | }
15 |
16 | if (typeof email === 'undefined') {
17 | username = username.toLowerCase()
18 | try { user = await User.findOne({username: username}) }
19 | catch (err) { res.status(400).json({message: msg.search_err}) }
20 | if (user) res.status(200).json({exists: true})
21 | else res.status(200).json({exists: false})
22 | }
23 | else if (typeof username === 'undefined') {
24 | try { user = await User.findOne({email: email}) }
25 | catch (err) { res.status(400).json({message: msg.search_err}) }
26 | if (user) res.status(200).json({exists: true})
27 | else res.status(200).json({exists: false})
28 | }
29 | }
30 |
31 | export { check }
--------------------------------------------------------------------------------
/server/controllers/image_search.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import { commons } from '../static/message.js';
3 | import { shuffleArray, unsplash } from '../util/util.js';
4 |
5 | const search = async (req, res, next) => {
6 |
7 | const { keyword } = req.query
8 | const pages = 3
9 | const images = []
10 | const splitArrays = []
11 |
12 | if (typeof keyword === 'undefined') {
13 | res.status(500).json({
14 | message: commons.invalid_params,
15 | format: "keyword"
16 | })
17 | return next()
18 | }
19 |
20 | for(let i=0; i {
28 | images.push({
29 | id: nanoid(),
30 | url: each.urls.small
31 | })
32 | })
33 | }
34 |
35 | shuffleArray(images)
36 |
37 | for(let i=0; i<64; i+=16) {
38 | splitArrays.push(images.slice(i, i+16))
39 | }
40 |
41 | res.send(splitArrays)
42 | }
43 |
44 | export { search }
--------------------------------------------------------------------------------
/server/controllers/verify_token.js:
--------------------------------------------------------------------------------
1 | import { usertModel } from '../models/user.js';
2 | import { userAttemptsModel } from '../models/user_attempts.js';
3 | import { commons, validation_messages as msg } from '../static/message.js';
4 | import * as path from 'path'
5 |
6 | const verify = async (req, res, next) => {
7 |
8 | const {email, token} = req.query
9 | let user
10 |
11 | if (typeof token === 'undefined' && typeof email === 'undefined') {
12 | res.status(500).json({
13 | message: commons.invalid_params,
14 | format: "token, email"
15 | })
16 | return next()
17 | }
18 |
19 | try { user = await usertModel.findOne({email: email}) }
20 | catch (err) { res.status(400).json({message: msg.search_err}); return next() }
21 | if (!user) {res.status(500).json({message: "User does not exists."}); return next()}
22 |
23 | const currentUser = await userAttemptsModel.findOne({email: email})
24 | const storedToken = currentUser.token
25 |
26 | if (storedToken === token) {
27 | await userAttemptsModel.findOneAndUpdate({email: email}, {attempts: 0, token: ""}).catch(err => console.log(err))
28 | res.sendFile(path.resolve() + '/views/unblocked.html')
29 | }
30 | else {
31 | res.send("Account is not blocked.")
32 | }
33 |
34 | }
35 |
36 | export {verify}
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Graphical Password Authentication Client
2 |
3 | Graphical Password Authentication Client is a React-based web application that enables users to create and authenticate with image passwords in combination with alphanumeric passwords. This project is built with ReactJS and styled with Tailwind CSS. The client makes API calls to the server using Axios.
4 |
5 | ## Installation
6 | To install this project, follow these steps:
7 |
8 | 1. Clone the repository to your local machine.
9 |
10 | ```bash
11 | git clone https://github.com/prathamesh-a/graphical-password-authentication.git
12 | ```
13 |
14 | 2. Navigate to the client folder inside the root project location.
15 |
16 | ```bash
17 | cd graphical-password-authentication/client
18 | ```
19 |
20 | 3. Run npm install to install the necessary dependencies.
21 |
22 | ```bash
23 | npm install
24 | ```
25 |
26 | ## Usage
27 | To use the client, follow these steps:
28 |
29 | 1. Start the client by running npm start .
30 |
31 | ```sql
32 | npm start
33 | ```
34 | 2. Open your web browser and navigate to http://localhost:3000/ to access the client.
35 | 3. Use the client to register a new account, log in with an existing account, and create an image password by selecting a set of images from the pre-defined set.
36 |
--------------------------------------------------------------------------------
/SCREENSHOTS.md:
--------------------------------------------------------------------------------
1 | # Screenshots
2 |
3 | ### Landing Page
4 | 
5 |
6 | ### Registration
7 |
8 | 
9 | 
10 |
11 | ### Login
12 |
13 | 
14 | 
15 |
16 | ### Authentication Success
17 | 
18 |
19 | ### Authentication Fail
20 | 
21 |
22 | ### Account Blocked
23 | 
24 |
25 | ### Notification Email
26 | 
27 |
28 | ### Account Unblock
29 | 
30 |
31 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Graphical Password Authentication Server
2 |
3 | Graphical Password Authentication Server is a Node.js-based web application that provides API endpoints for the client to create and authenticate with image passwords in combination with alphanumeric passwords. This project is built with Express.js and uses MongoDB for database storage.
4 |
5 | ## Installation
6 | To install this project, follow these steps:
7 |
8 | 1. Clone the repository to your local machine.
9 |
10 | ```bash
11 | git clone https://github.com/prathamesh-a/graphical-password-authentication.git
12 | ```
13 |
14 | 2. Navigate to the server folder inside the root project location.
15 |
16 | ```bash
17 | cd graphical-password-authentication/server
18 | ```
19 |
20 | 3. Run npm install to install the necessary dependencies.
21 |
22 | ```bash
23 | npm install
24 | ```
25 |
26 | ## Usage
27 | To use the server, follow these steps:
28 |
29 | 1. Make sure to specify the options in the .env file.
30 |
31 | 2. Start the server by running npm start .
32 |
33 | ```sql
34 | npm start
35 | ```
36 |
37 | 3. The server will start running on port 8080 .
38 | 4. Use the client to register a new account, log in with an existing account, and create an image password by selecting a set of images from the pre-defined set.
39 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphical_pass_auth",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@fortawesome/fontawesome-svg-core": "^6.2.1",
7 | "@fortawesome/free-regular-svg-icons": "^6.2.1",
8 | "@fortawesome/free-solid-svg-icons": "^6.2.1",
9 | "@fortawesome/react-fontawesome": "^0.2.0",
10 | "@testing-library/jest-dom": "^5.16.5",
11 | "@testing-library/react": "^13.4.0",
12 | "@testing-library/user-event": "^13.5.0",
13 | "@vercel/analytics": "^0.1.11",
14 | "axios": "^1.2.2",
15 | "body-scroll-lock": "^4.0.0-beta.0",
16 | "crypto-js": "^4.1.1",
17 | "framer-motion": "^10.2.1",
18 | "nanoid": "^4.0.0",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-loader-spinner": "^5.3.4",
22 | "react-scripts": "5.0.1",
23 | "react-switch": "^7.0.0",
24 | "react-toastify": "^9.1.1",
25 | "simple-crypto-js": "^3.0.1",
26 | "unsplash-js": "^7.0.15",
27 | "validator": "^13.7.0",
28 | "web-vitals": "^2.1.4"
29 | },
30 | "scripts": {
31 | "start": "react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject"
35 | },
36 | "eslintConfig": {
37 | "extends": [
38 | "react-app",
39 | "react-app/jest"
40 | ]
41 | },
42 | "browserslist": {
43 | "production": [
44 | ">0.2%",
45 | "not dead",
46 | "not op_mini all"
47 | ],
48 | "development": [
49 | "last 1 chrome version",
50 | "last 1 firefox version",
51 | "last 1 safari version"
52 | ]
53 | },
54 | "devDependencies": {
55 | "tailwindcss": "^3.2.4"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/client/src/static/icons_data.js:
--------------------------------------------------------------------------------
1 | export var icons =
2 | [
3 | {
4 | "id": "hSESTXkWdZ6P",
5 | "name": "goku",
6 | "url": "https://img.icons8.com/stickers/100/null/son-goku.png",
7 | "selected": false
8 | },
9 | {
10 | "id": "aoVDoLI_jNe5",
11 | "name": "rick",
12 | "url": "https://img.icons8.com/stickers/100/null/rick-sanchez.png",
13 | "selected": false
14 | },
15 | {
16 | "id": "wau71yfDiopJ",
17 | "name": "popeye",
18 | "url": "https://img.icons8.com/stickers/100/null/popeye.png",
19 | "selected": false
20 | },
21 | {
22 | "id": "2Aj56b5ynwJ5",
23 | "name": "ninjaturtle",
24 | "url": "https://img.icons8.com/stickers/100/null/ninja-turtle.png",
25 | "selected": false
26 | },
27 | {
28 | "id": "tni_yees8A-W",
29 | "name": "mario",
30 | "url": "https://img.icons8.com/stickers/100/null/super-mario.png",
31 | "selected": false
32 | },
33 | {
34 | "id": "cECvyZW_9kSe",
35 | "name": "hulk",
36 | "url": "https://img.icons8.com/stickers/100/null/hulk.png",
37 | "selected": false
38 | },
39 | {
40 | "id": "gKgo-53BO68B",
41 | "name": "naruto",
42 | "url": "https://img.icons8.com/stickers/100/null/naruto.png",
43 | "selected": false
44 | },
45 | {
46 | "id": "wqYlm6RuSl3Q",
47 | "name": "vader",
48 | "url": "https://img.icons8.com/stickers/100/null/darth-vader.png",
49 | "selected": false
50 | },
51 | {
52 | "id": "0bKJGNTVgolu",
53 | "name": "avatar",
54 | "url": "https://img.icons8.com/stickers/100/null/avatar.png",
55 | "selected": false
56 | },
57 | ]
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import Navbar from "./components/Navbar";
2 | import {useState} from "react";
3 | import {Page} from "./util/config";
4 | import Home from "./components/Landing";
5 | import Signup from "./components/Signup";
6 | import Footer from "./components/Footer";
7 | import Login from "./components/Login";
8 | import Loader from "./components/Items/Loader";
9 | import Contact from "./components/Contact";
10 | import Slider from "./components/Slider";
11 |
12 | function App() {
13 |
14 | const [page, setPage] = useState("home")
15 | const [loading, setLoading] = useState(false)
16 | const [loggedIn, setLoggedIn] = useState(false)
17 | const [slider, setSlider] = useState(false)
18 | const [userInfo, setUserInfo] = useState({
19 | username: "",
20 | email: ""
21 | })
22 |
23 | function getCurrentPage() {
24 | switch (page) {
25 | case Page.CONTACT:
26 | return
27 | case Page.LOGIN_PAGE:
28 | return
29 | case Page.SIGNUP_PAGE:
30 | return
31 | case Page.HOME_PAGE:
32 | default:
33 | return
34 | }
35 | }
36 |
37 | return (
38 |
39 | { loading &&
}
40 |
41 | {slider && }
42 |
43 | {getCurrentPage()}
44 |
46 |
47 |
48 | );
49 | }
50 | export default App;
--------------------------------------------------------------------------------
/server/util/util.js:
--------------------------------------------------------------------------------
1 | //import * as dotenv from 'dotenv'
2 | import fetch from "node-fetch";
3 | import { createApi } from "unsplash-js";
4 | import { userAttemptsModel } from "../models/user_attempts.js";
5 | import { transporter } from "./nodemailer.js";
6 |
7 | //dotenv.config()
8 |
9 | function checkArray(arr1, arr2, sequence) {
10 | if (arr1.length != arr2.length) return false;
11 | var gflag = false;
12 | if (sequence){
13 | for(let i=0; i 0; i--) {
40 | var j = Math.floor(Math.random() * (i + 1));
41 | var temp = array[i];
42 | array[i] = array[j];
43 | array[j] = temp;
44 | }
45 | }
46 |
47 | async function sendEmail(email) {
48 | const currentUser = await userAttemptsModel.findOne({email: email})
49 | const mailOptions = {
50 | from: "graphicalpassauth@gmail.com",
51 | to: email,
52 | subject: "GPA | Account Blocked",
53 | html: `
54 |
Your account has been blocked for multiple attempts of login with invalid credentials.
55 |
Click the link below to unblock:
56 |
Unblock
57 |
`
58 | }
59 | console.log("Sending email to " + email)
60 | transporter.sendMail(mailOptions, function(err, info) {
61 | if (err) console.log(err)
62 | else console.log("Email sent to " + email)
63 | })
64 | }
65 |
66 | export {checkArray, unsplash, shuffleArray, sendEmail}
--------------------------------------------------------------------------------
/client/src/components/Digest.js:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 | import validator from "validator/es";
3 | import {successToast, Toast} from "../util/toast";
4 | import axios from "axios";
5 | import {api} from "../static/config";
6 |
7 | export default function Digest() {
8 |
9 | const[email, setEMail] = useState("")
10 |
11 | function handleChange(event) {
12 | setEMail(event.target.value)
13 | }
14 |
15 | function handleSubmit() {
16 | if (validator.isEmail(email)) {
17 | axios.post(`${api.url}/api/digest`, {email: email})
18 | .then(() => {
19 | successToast("Thank You For Subscribing!")
20 | clearData()
21 | })
22 | .catch(err => {
23 | Toast(err.response.data.message)
24 | clearData()
25 | })
26 | }
27 | else Toast("Invalid Email")
28 |
29 | }
30 |
31 | function clearData() {
32 | setEMail("")
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Join Our Monthly Digest
43 |
Get Exclusive Promotions & Updates Staight To Your Box
44 |
45 |
46 |
47 | Subscribe
48 |
49 |
50 |
51 |
52 | )
53 | }
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | // import * as dotenv from 'dotenv'
2 | // dotenv.config()
3 | import bodyParser from 'body-parser'
4 | import express from 'express'
5 | import cors from 'cors'
6 | import mongoose from 'mongoose'
7 | import swaggerUi from 'swagger-ui-express'
8 | import fs from 'fs/promises'
9 | import { VerifyRoute } from './routes/verify.js'
10 | import { DigestRoutes } from './routes/digest.js'
11 | import { router as contactRoutes } from './routes/contact.js'
12 | import { router as imageRoutes } from './routes/image.js'
13 | import { router as userRoutes } from './routes/users.js'
14 |
15 | console.log(process.env)
16 |
17 | const app = express()
18 | const swaggerDocument = JSON.parse(
19 | await fs.readFile(
20 | new URL('./swagger.json', import.meta.url)
21 | )
22 | )
23 |
24 | app.use(cors())
25 | app.use(bodyParser.json())
26 |
27 | app.use('/api/verify', VerifyRoute)
28 | app.use('/api/user/', userRoutes)
29 | app.use('/api/image/', imageRoutes)
30 | app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument))
31 | app.use('/api/contact', contactRoutes)
32 | app.use('/api/digest', DigestRoutes)
33 |
34 | mongoose.set('strictQuery', true)
35 | mongoose
36 | .connect(`mongodb+srv://${process.env.DB_USERNAME}:${process.env.DB_PASSWORD}@${process.env.DB_NAME}.ajnurbv.mongodb.net/?retryWrites=true&w=majority`)
37 | .then(() => {
38 | app.listen(process.env.PORT)
39 | console.log("Server running...")
40 | })
41 | .catch(err => console.log(err))
42 |
43 |
44 | // const currentAttempts = await userAttemptsModel.findOne({email: "test@gmail.com"})
45 | // userAttemptsModel.findOneAndUpdate({email: "test@gmail.com", attempts: currentAttempts.attempts+1}).then(res => console.log(res)).catch(err => console.log(err))
46 |
47 | // await usertModel.findOne({username: "test"})
48 |
49 | // const testAttempts = new userAttemptsModel({
50 | // username: "test2",
51 | // email: "test2@gmail.com",
52 | // attempts: 0
53 | // })
54 |
55 | //testAttempts.save().then(res => console.log(res)).catch(err => console.log(err))
56 |
57 | // transporter.sendMail(mailOptions, function(err, info) {
58 | // if (err) console.log(err)
59 | // else console.log("Email Sent: " + info.response)
60 | // })
61 |
62 | // const result = unsplash.search.getPhotos({
63 | // query: 'cats',
64 | // perPage: 64,
65 | // orientation: 'squarish'
66 | // }).then(result => console.log(result.response.results))
--------------------------------------------------------------------------------
/server/controllers/users_signup.js:
--------------------------------------------------------------------------------
1 | //import * as dotenv from 'dotenv'
2 | import { usertModel as User } from '../models/user.js'
3 | import bcrypt from "bcryptjs"
4 | import { commons, signup_messages as msg } from '../static/message.js'
5 | import jwt from 'jsonwebtoken'
6 | import { userAttemptsModel } from '../models/user_attempts.js'
7 |
8 | //dotenv.config()
9 |
10 | const signup = async (req, res, next) => {
11 |
12 | let token
13 | let existingUser
14 | let hashedPassword
15 | var { username, email, password, pattern, sets, sequence} = req.body
16 | username = username.toLowerCase()
17 |
18 | if (typeof sets === 'undefined' || typeof username === 'undefined' || typeof email === 'undefined' || typeof password === 'undefined' || typeof pattern === 'undefined') {
19 | res.status(406).json({
20 | message: commons.invalid_params,
21 | format: msg.format
22 | })
23 | return
24 | }
25 |
26 | try { existingUser = await User.findOne({email: email}) }
27 | catch(err) {
28 | res.status(500).json({message: msg.db_user_failed})
29 | return next()
30 | }
31 |
32 | if (existingUser) {
33 | res.status(500).json({message: msg.user_already_exist})
34 | return next()
35 | }
36 |
37 | try { hashedPassword = await bcrypt.hash(password, 12) }
38 | catch(err) {
39 | res.status(500).json({message: msg.pass_hash_err})
40 | return next()
41 | }
42 |
43 | const createdUser = new User({
44 | username, email, password: hashedPassword, sets, pattern, sequence:false
45 | })
46 |
47 | const attempts = new userAttemptsModel({
48 | username, email, attempts: 0
49 | })
50 |
51 | try { await createdUser.save() }
52 | catch (err) {
53 | console.log(err)
54 | res.status(500).json({message: msg.db_save_err})
55 | return next()
56 | }
57 |
58 | try { await attempts.save() }
59 | catch (err) {
60 | console.log(err)
61 | res.status(500).json({message: msg.db_save_err})
62 | return next()
63 | }
64 |
65 | try { token = jwt.sign({userId: createdUser.id, email: createdUser.email}, process.env.TOKEN_KEY) }
66 | catch (err) {
67 | res.status(500).json({message: commons.token_failed})
68 | return next()
69 | }
70 |
71 | res.status(200).json({ username: createdUser.username, userId: createdUser.id, email: createdUser.email, token: token })
72 | }
73 |
74 | export {signup as signupController}
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
30 |
31 |
32 |
33 |
34 | Graphical Password Auth
35 |
36 |
37 | You need to enable JavaScript to run this app.
38 |
39 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/client/src/components/Slider.js:
--------------------------------------------------------------------------------
1 | import {Page} from "../util/config";
2 | import {motion} from "framer-motion";
3 |
4 | export default function Slider(props) {
5 |
6 | const additionalClasses = "text-[#A259FF]"
7 |
8 | function closeSlider() {
9 | props.setSlider(false)
10 | }
11 |
12 | function setPage(page) {
13 | props.setPage(page)
14 | closeSlider()
15 | }
16 |
17 | function logout() {
18 | props.setUserInfo({username: "", email: ""})
19 | props.setLoggedIn(false)
20 | setPage(Page.HOME_PAGE)
21 | }
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {!props.loggedIn &&
33 | setPage(Page.LOGIN_PAGE)} className="mb-6 transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 border-[#A259FF] border-2 hover:bg-transparent">Login
34 | setPage(Page.SIGNUP_PAGE)} className="transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 border-[#A259FF] border-2 hover:bg-transparent">Sign Up
35 |
}
36 |
37 | {props.loggedIn &&
38 |
{props.userInfo.username}
39 |
logout()} className="mt-4 w-1/3 transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 border-[#A259FF] border-2 hover:bg-transparent">Logout
40 |
}
41 |
42 |
43 |
setPage(Page.HOME_PAGE)} className={`mb-6 ${props.currentPage === Page.HOME_PAGE ? additionalClasses : ""}`}>Home
44 |
setPage(Page.CONTACT)} className={`${props.currentPage === Page.CONTACT ? additionalClasses : ""}`}>Contact
45 |
46 |
47 |
48 | )
49 | }
--------------------------------------------------------------------------------
/client/src/components/Navbar.js:
--------------------------------------------------------------------------------
1 | import {Page} from "../util/config";
2 |
3 | export default function Navbar(props) {
4 |
5 | const additionalClasses = "text-[#A259FF]"
6 |
7 | function setPage(property) {
8 | props.setPage(property)
9 | }
10 |
11 | function logout() {
12 | props.setUserInfo({username: "", email: ""})
13 | props.setLoggedIn(false)
14 | setPage(Page.HOME_PAGE)
15 | }
16 |
17 | return (
18 |
19 |
20 | {/*logo and text*/}
21 |
window.location.reload()}>
22 |
23 |
Graphical Password Auth
24 |
25 |
26 | {/*nav element list*/}
27 |
28 |
setPage(Page.HOME_PAGE)}>Home
29 | {/*
About Us
*/}
30 |
setPage(Page.CONTACT)}>Contact
31 |
32 | {!props.loggedIn &&
33 | setPage(Page.LOGIN_PAGE)} className="transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 ml-12 border-[#A259FF] border-2 hover:bg-transparent">Login
34 | setPage(Page.SIGNUP_PAGE)} className="transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 ml-6 border-[#A259FF] border-2 hover:bg-transparent">Sign Up
35 |
}
36 |
37 | {props.loggedIn &&
38 |
{props.userInfo.username}
39 |
logout()} className="transition duration-500 ease-in-out bg-[#A259FF] rounded-lg px-4 py-1 ml-4 border-[#A259FF] border-2 hover:bg-transparent">Logout
40 |
}
41 |
42 |
43 |
44 |
props.setSlider(true)} className="ml-2" width="32px" src="https://img.icons8.com/fluency-systems-regular/48/A259FF/menu--v1.png" alt=""/>
45 |
46 |
47 |
48 | )
49 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | Graphical Password Authentication is a GitHub project that provides an additional layer of security to alphanumeric passwords by using images as passwords. With this project, users can create a unique and personalized image password by selecting images from a pre-defined set. This password can then be used in combination with a traditional alphanumeric password for enhanced security. This project is ideal for applications where password protection is critical, such as online banking, e-commerce, or social media. By providing an additional layer of authentication, this project can significantly reduce the risk of unauthorized access to sensitive information. This project uses the MERN stack (MongoDB, Express, React, Node.js) to build a server and a client for the application.
12 |
13 | ## Screenshots
14 | To view screenshots go to SCREENSHOTS .
15 |
16 | ## Installation
17 | To install this project, follow these steps:
18 |
19 | 1. Clone the repository to your local machine.
20 |
21 | ```bash
22 | git clone https://github.com/prathamesh-a/graphical-password-authentication.git
23 | ```
24 |
25 | 2. Navigate to the root folder of the project.
26 |
27 | ```bash
28 | git clone https://github.com/prathamesh-a/graphical-password-authentication.git
29 | ```
30 |
31 | 3. Installation of Server
32 | 4. Installation of Client
33 |
34 | ## Usage
35 | To use the application, follow these steps:
36 |
37 | 1. Setup Server
38 | 2. Setup Client
39 |
40 | ## Contributing
41 | If you would like to contribute to this project, please follow these steps:
42 |
43 | 1. Fork the repository to your own account.
44 | 2. Create a new branch from the **`develop`** branch.
45 | 3. Make your changes and test them thoroughly.
46 | 4. Submit a pull request to the **`develop`** branch.
47 |
48 | ## License
49 | This project is licensed under the MIT License. See the LICENSE file for details.
50 |
--------------------------------------------------------------------------------
/server/controllers/users_login.js:
--------------------------------------------------------------------------------
1 | //import * as dotenv from 'dotenv'
2 | import { usertModel as User } from '../models/user.js'
3 | import bcrypt from "bcryptjs"
4 | import { login_messages as msg, commons} from '../static/message.js'
5 | import jwt from 'jsonwebtoken'
6 | import { checkArray, sendEmail } from '../util/util.js'
7 | import { userAttemptsModel } from '../models/user_attempts.js'
8 | import { nanoid } from 'nanoid'
9 |
10 |
11 | const login = async (req, res, next) => {
12 |
13 | //dotenv.config()
14 |
15 | let token
16 | let existingUser
17 | let isValidPassword = false
18 | var isValidPattern = false
19 | var { username, password, pattern } = req.body
20 | username = username.toLowerCase()
21 |
22 | if (typeof username === 'undefined' || typeof password === 'undefined' || typeof pattern === 'undefined') {
23 | res.status(406).json({
24 | message: commons.invalid_params,
25 | format: msg.format
26 | })
27 | return next()
28 | }
29 |
30 | try { existingUser = await User.findOne({username: username}) }
31 | catch(err) {
32 | res.status(401).json({message: msg.db_user_failed})
33 | return next()
34 | }
35 |
36 | if (!existingUser) {
37 | res.status(401).json({message: msg.user_not_exist})
38 | return next()
39 | }
40 |
41 | const currentAttempts = await userAttemptsModel.findOne({username: username})
42 |
43 | if (currentAttempts.attempts > process.env.MAX_ATTEMPTS) {
44 | res.status(500).json({status: "blocked", message: "Your account has been blocked, please check email."})
45 | return next()
46 | }
47 |
48 | try { isValidPassword = await bcrypt.compare(password, existingUser.password) }
49 | catch(err) {
50 | console.log(err)
51 | res.status(500).json({message: msg.db_pass_failed})
52 | return next()
53 | }
54 |
55 | isValidPattern = checkArray(existingUser.pattern, pattern, true)
56 |
57 | if (!isValidPassword || !isValidPattern) {
58 | if (currentAttempts.attempts === Number(process.env.MAX_ATTEMPTS)) {
59 | await userAttemptsModel.findOneAndUpdate({username: username}, {attempts: currentAttempts.attempts+1, token: nanoid(32)}).catch(err => console.log(err))
60 | //console.log("sending email entered")
61 | sendEmail(currentAttempts.email)
62 | }
63 | await userAttemptsModel.findOneAndUpdate({username: username}, {attempts: currentAttempts.attempts+1}).catch(err => console.log(err))
64 | res.status(500).json({message: msg.invalid_credentials})
65 | return next()
66 | }
67 |
68 | try { token = jwt.sign({userId: existingUser.id, email: existingUser.email}, process.env.TOKEN_KEY) }
69 | catch (err) {
70 | console.log(err)
71 | res.status(500).json({message: commons.token_failed})
72 | return next()
73 | }
74 | await userAttemptsModel.findOneAndUpdate({username: username}, {attempts: 0}).catch(err => console.log(err))
75 | res.status(200).json({username: existingUser.username, userId: existingUser.id, email: existingUser.email, token: token})
76 | }
77 |
78 | export {login as loginController}
--------------------------------------------------------------------------------
/client/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import {Page} from "../util/config";
2 | import {useState} from "react";
3 | import validator from "validator/es";
4 | import {successToast, Toast} from "../util/toast";
5 | import axios from "axios";
6 | import {api} from "../static/config";
7 |
8 | export default function Footer(props) {
9 |
10 | const[email, setEMail] = useState("")
11 |
12 | function handleChange(event) {
13 | setEMail(event.target.value)
14 | }
15 |
16 | function handleSubmit() {
17 | if (validator.isEmail(email)) {
18 | axios.post(`${api.url}/api/digest`, {email: email})
19 | .then(() => {
20 | successToast("Thank You For Subscribing!")
21 | clearData()
22 | })
23 | .catch(err => {
24 | Toast(err.response.data.message)
25 | clearData()
26 | })
27 | }
28 | else Toast("Invalid Email")
29 | }
30 |
31 | function clearData() {
32 | setEMail("")
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Graphical Password Auth
43 |
44 |
A Novel Approach For Security
45 |
46 |
47 |
48 |
Explore
49 |
props.setPage(Page.ABOUT)} className="text-gray-300 font-['Work_Sans'] mt-2 sm:mt-4 cursor-pointer">About Us
50 |
props.setPage(Page.CONTACT)} className="text-gray-300 font-['Work_Sans'] cursor-pointer">Contact
51 |
52 |
53 |
54 |
Join Our Monthly Digest
55 |
Get Exclusive Promotions & Updates.
56 |
57 |
58 |
59 |
60 | Subscribe
61 |
62 |
63 |
64 |
65 |
66 |
github.com/prathamesh-a
67 |
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/client/src/components/Contact.js:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 | import validator from "validator/es";
3 | import {successToast, Toast} from "../util/toast";
4 | import axios from "axios";
5 | import {api} from "../static/config";
6 |
7 | export default function Contact(props) {
8 |
9 | const [data, setData] = useState({
10 | name: "",
11 | email: "",
12 | message: ""
13 | })
14 |
15 | function handleChange(event) {
16 | setData(prev => {
17 | return {
18 | ...prev,
19 | [event.target.name]: event.target.value
20 | }
21 | })
22 | }
23 |
24 | function handleSubmit() {
25 | if (!validateData()) return
26 | props.setLoading(true)
27 | axios.post(`${api.url}/api/contact`, data)
28 | .then(res => {
29 | props.setLoading(false)
30 | successToast("Message Sent")
31 | clearData()
32 | })
33 | .catch(err => Toast(err.response.data.message))
34 | }
35 |
36 | function clearData() {
37 | setData({
38 | name: "",
39 | email: "",
40 | message: ""
41 | })
42 | }
43 |
44 | function validateData() {
45 | if (data.name.length < 3) {
46 | Toast("Invalid Name")
47 | return false
48 | }
49 | if (!validator.isEmail(data.email)) {
50 | Toast("Invalid Email")
51 | return false
52 | }
53 | if (data.message.length < 3) {
54 | Toast("Enter a valid message")
55 | return false
56 | }
57 | return true
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 |
Connect With Us
65 |
We would love to respond to your queries.
66 |
Feel free to get in touch with us.
67 |
81 |
Submit
82 |
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
--------------------------------------------------------------------------------
/client/src/components/Landing.js:
--------------------------------------------------------------------------------
1 | import {faUnlock} from "@fortawesome/free-solid-svg-icons";
2 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
3 | import AttackBlock from "./Items/AttackBlock";
4 | import Digest from "./Digest";
5 |
6 | export default function Home() {
7 |
8 | function handleKnowMore() {
9 | const element = document.getElementById('home--2')
10 | if (element) element.scrollIntoView({behavior: "smooth"})
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {/*INFO*/}
22 |
23 |
Discover
24 |
Graphical Password
25 |
Authentication
26 |
A Novel Approach For Security
27 |
And User Experience Of
28 |
Graphical Password Authentication.
29 |
30 |
31 | Know More
32 |
33 |
34 | {/*IMAGE*/}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
Resistance To Attacks
43 |
Our System Provides Security Against Popular Attacks.
44 |
66 |
67 |
68 |
69 |
70 | )
71 | }
--------------------------------------------------------------------------------
/server/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.1",
3 | "info": {
4 | "title": "Graphical Authentication System API",
5 | "description": "API documentation for the Graphical Authentication System",
6 | "license": {
7 | "name": "MIT",
8 | "url": "https://opensource.org/licenses/MIT"
9 | },
10 | "version": "0.1.0"
11 | },
12 | "paths": {
13 | "/api/user/login": {
14 | "post": {
15 | "summary": "login user into application",
16 | "responses": {
17 | "200": {
18 | "description": "Login of user is successful."
19 | },
20 | "406": {
21 | "description": "The given parameters are not in valid format."
22 | },
23 | "500": {
24 | "description": "The given parameters are not valid."
25 | }
26 | },
27 | "requestBody": {
28 | "content": {
29 | "application/json": {
30 | "schema": {
31 | "type": "object",
32 | "properties": {
33 | "username": {
34 | "type": "string"
35 | },
36 | "password": {
37 | "type": "string"
38 | },
39 | "pattern": {
40 | "type": "array",
41 | "items": {
42 | "type": "string"
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | },
52 | "/api/user/signup": {
53 | "post": {
54 | "summary": "register new user into application",
55 | "responses": {
56 | "200": {
57 | "description": "Signup of new user is successful."
58 | },
59 | "406": {
60 | "description": "The given parameters are not in valid format."
61 | },
62 | "500": {
63 | "description": "The given parameters are not valid."
64 | }
65 | },
66 | "requestBody": {
67 | "content": {
68 | "application/json": {
69 | "schema": {
70 | "type": "object",
71 | "properties": {
72 | "username": {
73 | "type": "string"
74 | },
75 | "email": {
76 | "type": "string"
77 | },
78 | "password": {
79 | "type": "string"
80 | },
81 | "pattern": {
82 | "type": "array",
83 | "items": {
84 | "type": "string"
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
91 | }
92 | }
93 | },
94 | "/api/user/check/email": {
95 | "post": {
96 | "summary": "check for duplicate email",
97 | "responses": {
98 | "200": {
99 | "description": "Check for email is successful.",
100 | "content": {
101 | "application/json": {
102 | "schema": {
103 | "type": "object",
104 | "properties": {
105 | "exists": {
106 | "type": "boolean"
107 | }
108 | }
109 | }
110 | }
111 | }
112 | },
113 | "400": {
114 | "description": "Internal server error."
115 | },
116 | "500": {
117 | "description": "The given parameters are not in valid format."
118 | }
119 | },
120 | "requestBody": {
121 | "content": {
122 | "application/json": {
123 | "schema": {
124 | "type": "object",
125 | "properties": {
126 | "email": {
127 | "type": "string"
128 | }
129 | }
130 | }
131 | }
132 | }
133 | }
134 | }
135 | },
136 | "/api/user/check/username": {
137 | "post": {
138 | "summary": "check for duplicate username",
139 | "responses": {
140 | "200": {
141 | "description": "Check for username is successful.",
142 | "content": {
143 | "application/json": {
144 | "schema": {
145 | "type": "object",
146 | "properties": {
147 | "exists": {
148 | "type": "boolean"
149 | }
150 | }
151 | }
152 | }
153 | }
154 | },
155 | "400": {
156 | "description": "Internal server error."
157 | },
158 | "500": {
159 | "description": "The given parameters are not in valid format."
160 | }
161 | },
162 | "requestBody": {
163 | "content": {
164 | "application/json": {
165 | "schema": {
166 | "type": "object",
167 | "properties": {
168 | "username": {
169 | "type": "string"
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 | }
178 | }
179 | }
--------------------------------------------------------------------------------
/client/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 | import { checkUsername} from "../util/validation";
3 | import {successToast, Toast} from "../util/toast";
4 | import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
5 | import {faArrowLeft} from "@fortawesome/free-solid-svg-icons";
6 | import PasswordIcon from "./Items/PasswordIcon";
7 | import axios from "axios";
8 | import {Page} from "../util/config";
9 | import {api} from "../static/config";
10 | import {getNameByNumber} from "../util/util";
11 | import {nanoid} from "nanoid";
12 | import BlockedBox from "./Items/BlockedBox";
13 |
14 | export default function Login(props) {
15 |
16 | const [next, setNext] = useState(false)
17 | const [blocked, setBlocked] = useState(false)
18 | const [iteration, setIteration] = useState(0)
19 | const [imageData, setImageData] = useState([])
20 | const [loginInfo, setLoginInfo] = useState({
21 | username: "",
22 | password: "",
23 | pattern: ["", "", "", ""]
24 | })
25 |
26 | function handleChange(event) {
27 | setLoginInfo(prev => {
28 | return {
29 | ...prev,
30 | [event.target.name]: event.target.value
31 | }
32 | })
33 | }
34 |
35 | function validateData() {
36 | if (loginInfo.username.length < 1) {
37 | Toast("Invalid Username!")
38 | return false
39 | }
40 | else if (loginInfo.password.length < 8) {
41 | Toast("Password Length Must Be Greater Than 8")
42 | return false
43 | }
44 | return true
45 | }
46 |
47 | async function validateUsernameAndEmail() {
48 | const isUsernameExists = await checkUsername(loginInfo.username, props.setLoading)
49 | if (!isUsernameExists) Toast("Username does not exists!")
50 | return isUsernameExists
51 | }
52 |
53 | async function handleNextClick(event) {
54 | if (validateData() && await validateUsernameAndEmail()) {
55 | axios.get(`${api.url}/api/image?username=${loginInfo.username}`)
56 | .then(res => {
57 | setImageData(res.data)
58 | setNext(true)
59 | })
60 | .catch(err => Toast("Internal server error"))
61 | }
62 | }
63 |
64 | function getIcons() {
65 | return imageData[iteration].map(prev => )
66 | }
67 |
68 | function handleImageClick(id, iteration) {
69 | var newPattern = loginInfo.pattern
70 | newPattern[iteration] = id
71 | setLoginInfo(prev => {
72 | return {
73 | ...prev,
74 | "pattern": newPattern
75 | }
76 | })
77 | }
78 |
79 | function login() {
80 |
81 | if (loginInfo.pattern[iteration] === "") {
82 | Toast("Select an image first!")
83 | return
84 | }
85 |
86 | if (iteration < 3) {
87 | setIteration(iteration+1)
88 | return
89 | }
90 |
91 | if (loginInfo.pattern.length < 4) {
92 | Toast("Chose minimum 4 images!")
93 | return
94 | }
95 | props.setLoading(true)
96 | axios.post(`${api.url}/api/user/login`, loginInfo)
97 | .then(res => {
98 | props.setLoading(false)
99 | console.log(res.data)
100 | props.setUserInfo({email: res.data.email, username: res.data.username})
101 | props.setLoggedIn(true)
102 | successToast("Logged In!")
103 | props.setPage(Page.HOME_PAGE)
104 | })
105 | .catch(err => {
106 | props.setLoading(false)
107 | setIteration(0)
108 | setLoginInfo(prev => {
109 | return {
110 | ...prev,
111 | "pattern": ["", "", "", ""]
112 | }
113 | })
114 | setNext(false)
115 | if (typeof err.response.data.status != 'undefined' && err.response.data.status === 'blocked') {
116 | setBlocked(true)
117 | }
118 | else Toast(err.response.data.message)
119 | })
120 | }
121 |
122 | function getButtonTitle() {
123 | if (iteration < 3) return "Next"
124 | else return "Login"
125 | }
126 |
127 | function handleBackClick() {
128 | if (iteration === 0) setNext(false)
129 | else setIteration(iteration-1)
130 | }
131 |
132 | return (
133 |
134 |
135 | {blocked &&
}
136 |
137 | {!next &&
138 | {/*IMAGE*/}
139 |
140 |
141 |
142 | {/*LOGIN FORM*/}
143 |
144 |
Login
145 |
Welcome Back! Enter Your Details Below
146 |
147 |
148 |
149 |
150 |
151 |
Next
152 |
153 |
}
154 |
155 | {next &&
156 |
157 | {getIcons()}
158 |
159 |
160 | {/*DESKTOP VIEW*/}
161 |
162 |
Set Graphical Password
163 |
Select Images For Your Graphical Password.
164 |
Select {getNameByNumber(iteration+1)} Image.
165 |
{getButtonTitle()}
166 |
167 |
168 |
169 |
170 |
171 | {/*MOBILE VIEW*/}
172 |
173 |
Set Graphical Password
174 |
Select Images For Your Graphical Password.
175 |
Select {getNameByNumber(iteration+1)} Image.
176 |
177 |
178 | {getIcons()}
179 |
180 |
181 |
{getButtonTitle()}
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
}
190 |
191 |
192 | )
193 | }
--------------------------------------------------------------------------------
/client/src/components/Signup.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from "react";
2 | import PasswordIcon from "./Items/PasswordIcon";
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4 | import {faArrowLeft, faSearch} from '@fortawesome/free-solid-svg-icons'
5 | import validator from "validator/es";
6 | import axios from "axios";
7 | import {successToast, Toast} from "../util/toast";
8 | import {checkEmail, checkUsername} from "../util/validation";
9 | import {Page} from "../util/config";
10 | import {api} from "../static/config";
11 | import {getNameByNumber} from "../util/util";
12 | import {nanoid} from "nanoid";
13 |
14 | export default function Signup(props) {
15 |
16 | const [next, setNext] = useState(false)
17 | const [iteration, setIteration] = useState(0)
18 | const [keyword, setKeyword] = useState("")
19 | const [imageData, setImageData] = useState([])
20 | const [signupInfo, setSignupInfo] = useState({
21 | username: "",
22 | email: "",
23 | password: "",
24 | pattern: ["", "", "", ""],
25 | sets: [[]]
26 | })
27 |
28 | function handleChange(event) {
29 | setSignupInfo(prev => {
30 | return {
31 | ...prev,
32 | [event.target.name]: event.target.value
33 | }
34 | })
35 | }
36 |
37 | useEffect(function() {
38 | setSignupInfo(prev => {
39 | return {
40 | ...prev,
41 | "sets": imageData,
42 | "pattern": ["", "", "", ""]
43 | }
44 | })
45 | }, [imageData])
46 |
47 | function getIcons() {
48 | return imageData[iteration].map(prev => )
49 | }
50 |
51 | function handleImageClick(id, iteration) {
52 | var newPattern = signupInfo.pattern
53 | newPattern[iteration] = id
54 | setSignupInfo(prev => {
55 | return {
56 | ...prev,
57 | "pattern": newPattern
58 | }
59 | })
60 | }
61 |
62 | function createAccount() {
63 | if (signupInfo.pattern[iteration] === "") {
64 | Toast("Select an image first!")
65 | return
66 | }
67 |
68 | if (iteration < 3) {
69 | setIteration(iteration+1)
70 | return
71 | }
72 |
73 | if (signupInfo.pattern.length < 4) {
74 | Toast("Chose all 4 images!")
75 | return
76 | }
77 | props.setLoading(true)
78 | axios.post(`${api.url}/api/user/signup`, signupInfo)
79 | .then(res => {
80 | props.setLoading(false)
81 | console.log(res.data)
82 | props.setUserInfo({email: res.data.email, username: res.data.username})
83 | props.setLoggedIn(true)
84 | successToast("Logged In!")
85 | props.setPage(Page.HOME_PAGE)
86 | }
87 | )
88 | .catch(err => {
89 | console.log(err)
90 | props.setLoading(false)
91 | Toast(err.response.data.message)
92 | })
93 | }
94 |
95 | function validateData() {
96 | if (signupInfo.username.length < 1) {
97 | Toast("Invalid username!")
98 | return false
99 | }
100 | else if (!validator.isEmail(signupInfo.email)) {
101 | Toast("Invalid email address!")
102 | return false
103 | }
104 | else if (signupInfo.password.length < 8) {
105 | Toast("Password length should be more than 8")
106 | return false
107 | }
108 | return true
109 | }
110 |
111 | async function validateUsernameAndEmail() {
112 | const isEmailExist = await checkEmail(signupInfo.email, props.setLoading)
113 | const isUsernameExists = await checkUsername(signupInfo.username, props.setLoading)
114 |
115 | if (isUsernameExists) Toast("Username already exists!")
116 | else if (isEmailExist) Toast("Email already exists!")
117 |
118 | return !isEmailExist && !isUsernameExists
119 | }
120 |
121 | async function handleNextClick(event) {
122 | if (validateData() && await validateUsernameAndEmail()) {setNext(true)}
123 | }
124 |
125 | function searchKeyword() {
126 | if (keyword === "") {
127 | Toast("Invalid keyword!")
128 | return
129 | }
130 |
131 | props.setLoading(true)
132 | axios.get(`${api.url}/api/image/search?keyword=${keyword}`)
133 | .then(data => {
134 | props.setLoading(false)
135 | setImageData(data.data)
136 | })
137 | .catch(err => {
138 | console.log(err)
139 | props.setLoading(false)
140 | Toast(err.response.data.message)
141 | })
142 | }
143 |
144 | function getButtonTitle() {
145 | if (iteration < 3) return "Next"
146 | else return "Create Account"
147 | }
148 |
149 | function handleBackClick() {
150 | if (iteration === 0) setNext(false)
151 | else setIteration(iteration-1)
152 | }
153 |
154 | return (
155 |
156 | {!next &&
157 | {/*IMAGE*/}
158 |
159 |
160 |
161 | {/*SIGNUP FORM*/}
162 |
163 |
Create Account
164 |
Welcome! Enter Your Details And Experience
165 |
Graphical Password System.
166 |
167 |
168 |
169 |
170 |
171 |
Next
172 |
173 |
}
174 |
175 | {next &&
176 | {imageData.length > 0 &&
178 | {getIcons()}
179 |
}
180 | {imageData.length === 0 &&
}
184 |
185 | {/*DESKTOP VIEW*/}
186 |
187 |
Set Graphical Password
188 |
Enter keyword to get images.
189 |
Select {getNameByNumber(iteration+1)} Image.
190 |
191 | {iteration === 0 &&
192 |
Type Keyword:
193 |
194 | setKeyword(event.target.value)} value={keyword}
195 | placeholder="Try 'Cats'" className="rounded-l-md px-4 bg-gray-100 text-2xl py-1"/>
196 |
198 |
199 |
200 |
}
201 |
{getButtonTitle()}
202 |
203 |
204 |
205 |
206 |
207 | {/*MOBILE VIEW*/}
208 |
209 |
Set Graphical Password
210 |
Enter keyword to get images.
211 |
Select {getNameByNumber(iteration+1)} Image.
212 |
213 | {iteration === 0 &&
214 |
Type Keyword:
215 |
216 | setKeyword(event.target.value)} value={keyword}
217 | placeholder="Try 'Cats'" className="rounded-l-md px-2 bg-gray-100 h-8 text-lg py-0"/>
218 |
220 |
221 |
222 |
}
223 |
224 | {imageData.length > 0 &&
226 | {getIcons()}
227 |
}
228 | {imageData.length === 0 &&
}
232 |
233 |
{getButtonTitle()}
234 |
235 |
236 |
237 |
238 |
}
239 |
240 | )
241 | }
242 |
243 |
244 | // useEffect(function() {
245 | // const newPattern = signupInfo.pattern;
246 | // for(let i=0; i {
256 | // return {
257 | // ...prev,
258 | // "pattern": newPattern
259 | // }
260 | // })
261 | // }, [iconsData])
--------------------------------------------------------------------------------
/client/public/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | 5. Use the user's configured `sans` font-feature-settings by default.
34 | */
35 |
36 | html {
37 | line-height: 1.5;
38 | /* 1 */
39 | -webkit-text-size-adjust: 100%;
40 | /* 2 */
41 | /* 3 */
42 | tab-size: 4;
43 | /* 3 */
44 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
45 | /* 4 */
46 | -webkit-font-feature-settings: normal;
47 | font-feature-settings: normal;
48 | /* 5 */
49 | }
50 |
51 | /*
52 | 1. Remove the margin in all browsers.
53 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
54 | */
55 |
56 | body {
57 | margin: 0;
58 | /* 1 */
59 | line-height: inherit;
60 | /* 2 */
61 | }
62 |
63 | /*
64 | 1. Add the correct height in Firefox.
65 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
66 | 3. Ensure horizontal rules are visible by default.
67 | */
68 |
69 | hr {
70 | height: 0;
71 | /* 1 */
72 | color: inherit;
73 | /* 2 */
74 | border-top-width: 1px;
75 | /* 3 */
76 | }
77 |
78 | /*
79 | Add the correct text decoration in Chrome, Edge, and Safari.
80 | */
81 |
82 | abbr:where([title]) {
83 | -webkit-text-decoration: underline dotted;
84 | text-decoration: underline dotted;
85 | }
86 |
87 | /*
88 | Remove the default font size and weight for headings.
89 | */
90 |
91 | h1,
92 | h2,
93 | h3,
94 | h4,
95 | h5,
96 | h6 {
97 | font-size: inherit;
98 | font-weight: inherit;
99 | }
100 |
101 | /*
102 | Reset links to optimize for opt-in styling instead of opt-out.
103 | */
104 |
105 | a {
106 | color: inherit;
107 | text-decoration: inherit;
108 | }
109 |
110 | /*
111 | Add the correct font weight in Edge and Safari.
112 | */
113 |
114 | b,
115 | strong {
116 | font-weight: bolder;
117 | }
118 |
119 | /*
120 | 1. Use the user's configured `mono` font family by default.
121 | 2. Correct the odd `em` font sizing in all browsers.
122 | */
123 |
124 | code,
125 | kbd,
126 | samp,
127 | pre {
128 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
129 | /* 1 */
130 | font-size: 1em;
131 | /* 2 */
132 | }
133 |
134 | /*
135 | Add the correct font size in all browsers.
136 | */
137 |
138 | small {
139 | font-size: 80%;
140 | }
141 |
142 | /*
143 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
144 | */
145 |
146 | sub,
147 | sup {
148 | font-size: 75%;
149 | line-height: 0;
150 | position: relative;
151 | vertical-align: baseline;
152 | }
153 |
154 | sub {
155 | bottom: -0.25em;
156 | }
157 |
158 | sup {
159 | top: -0.5em;
160 | }
161 |
162 | /*
163 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
164 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
165 | 3. Remove gaps between table borders by default.
166 | */
167 |
168 | table {
169 | text-indent: 0;
170 | /* 1 */
171 | border-color: inherit;
172 | /* 2 */
173 | border-collapse: collapse;
174 | /* 3 */
175 | }
176 |
177 | /*
178 | 1. Change the font styles in all browsers.
179 | 2. Remove the margin in Firefox and Safari.
180 | 3. Remove default padding in all browsers.
181 | */
182 |
183 | button,
184 | input,
185 | optgroup,
186 | select,
187 | textarea {
188 | font-family: inherit;
189 | /* 1 */
190 | font-size: 100%;
191 | /* 1 */
192 | font-weight: inherit;
193 | /* 1 */
194 | line-height: inherit;
195 | /* 1 */
196 | color: inherit;
197 | /* 1 */
198 | margin: 0;
199 | /* 2 */
200 | padding: 0;
201 | /* 3 */
202 | }
203 |
204 | /*
205 | Remove the inheritance of text transform in Edge and Firefox.
206 | */
207 |
208 | button,
209 | select {
210 | text-transform: none;
211 | }
212 |
213 | /*
214 | 1. Correct the inability to style clickable types in iOS and Safari.
215 | 2. Remove default button styles.
216 | */
217 |
218 | button,
219 | [type='button'],
220 | [type='reset'],
221 | [type='submit'] {
222 | -webkit-appearance: button;
223 | /* 1 */
224 | background-color: transparent;
225 | /* 2 */
226 | background-image: none;
227 | /* 2 */
228 | }
229 |
230 | /*
231 | Use the modern Firefox focus style for all focusable elements.
232 | */
233 |
234 | :-moz-focusring {
235 | outline: auto;
236 | }
237 |
238 | /*
239 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
240 | */
241 |
242 | :-moz-ui-invalid {
243 | box-shadow: none;
244 | }
245 |
246 | /*
247 | Add the correct vertical alignment in Chrome and Firefox.
248 | */
249 |
250 | progress {
251 | vertical-align: baseline;
252 | }
253 |
254 | /*
255 | Correct the cursor style of increment and decrement buttons in Safari.
256 | */
257 |
258 | ::-webkit-inner-spin-button,
259 | ::-webkit-outer-spin-button {
260 | height: auto;
261 | }
262 |
263 | /*
264 | 1. Correct the odd appearance in Chrome and Safari.
265 | 2. Correct the outline style in Safari.
266 | */
267 |
268 | [type='search'] {
269 | -webkit-appearance: textfield;
270 | /* 1 */
271 | outline-offset: -2px;
272 | /* 2 */
273 | }
274 |
275 | /*
276 | Remove the inner padding in Chrome and Safari on macOS.
277 | */
278 |
279 | ::-webkit-search-decoration {
280 | -webkit-appearance: none;
281 | }
282 |
283 | /*
284 | 1. Correct the inability to style clickable types in iOS and Safari.
285 | 2. Change font properties to `inherit` in Safari.
286 | */
287 |
288 | ::-webkit-file-upload-button {
289 | -webkit-appearance: button;
290 | /* 1 */
291 | font: inherit;
292 | /* 2 */
293 | }
294 |
295 | /*
296 | Add the correct display in Chrome and Safari.
297 | */
298 |
299 | summary {
300 | display: list-item;
301 | }
302 |
303 | /*
304 | Removes the default spacing and border for appropriate elements.
305 | */
306 |
307 | blockquote,
308 | dl,
309 | dd,
310 | h1,
311 | h2,
312 | h3,
313 | h4,
314 | h5,
315 | h6,
316 | hr,
317 | figure,
318 | p,
319 | pre {
320 | margin: 0;
321 | }
322 |
323 | fieldset {
324 | margin: 0;
325 | padding: 0;
326 | }
327 |
328 | legend {
329 | padding: 0;
330 | }
331 |
332 | ol,
333 | ul,
334 | menu {
335 | list-style: none;
336 | margin: 0;
337 | padding: 0;
338 | }
339 |
340 | /*
341 | Prevent resizing textareas horizontally by default.
342 | */
343 |
344 | textarea {
345 | resize: vertical;
346 | }
347 |
348 | /*
349 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
350 | 2. Set the default placeholder color to the user's configured gray 400 color.
351 | */
352 |
353 | input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
354 | opacity: 1;
355 | /* 1 */
356 | color: #9ca3af;
357 | /* 2 */
358 | }
359 |
360 | input::placeholder,
361 | textarea::placeholder {
362 | opacity: 1;
363 | /* 1 */
364 | color: #9ca3af;
365 | /* 2 */
366 | }
367 |
368 | /*
369 | Set the default cursor for buttons.
370 | */
371 |
372 | button,
373 | [role="button"] {
374 | cursor: pointer;
375 | }
376 |
377 | /*
378 | Make sure disabled buttons don't get the pointer cursor.
379 | */
380 |
381 | :disabled {
382 | cursor: default;
383 | }
384 |
385 | /*
386 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
387 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
388 | This can trigger a poorly considered lint error in some tools but is included by design.
389 | */
390 |
391 | img,
392 | svg,
393 | video,
394 | canvas,
395 | audio,
396 | iframe,
397 | embed,
398 | object {
399 | display: block;
400 | /* 1 */
401 | vertical-align: middle;
402 | /* 2 */
403 | }
404 |
405 | /*
406 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
407 | */
408 |
409 | img,
410 | video {
411 | max-width: 100%;
412 | height: auto;
413 | }
414 |
415 | /* Make elements with the HTML hidden attribute stay hidden by default */
416 |
417 | [hidden] {
418 | display: none;
419 | }
420 |
421 | *, ::before, ::after {
422 | --tw-border-spacing-x: 0;
423 | --tw-border-spacing-y: 0;
424 | --tw-translate-x: 0;
425 | --tw-translate-y: 0;
426 | --tw-rotate: 0;
427 | --tw-skew-x: 0;
428 | --tw-skew-y: 0;
429 | --tw-scale-x: 1;
430 | --tw-scale-y: 1;
431 | --tw-pan-x: ;
432 | --tw-pan-y: ;
433 | --tw-pinch-zoom: ;
434 | --tw-scroll-snap-strictness: proximity;
435 | --tw-ordinal: ;
436 | --tw-slashed-zero: ;
437 | --tw-numeric-figure: ;
438 | --tw-numeric-spacing: ;
439 | --tw-numeric-fraction: ;
440 | --tw-ring-inset: ;
441 | --tw-ring-offset-width: 0px;
442 | --tw-ring-offset-color: #fff;
443 | --tw-ring-color: rgb(59 130 246 / 0.5);
444 | --tw-ring-offset-shadow: 0 0 #0000;
445 | --tw-ring-shadow: 0 0 #0000;
446 | --tw-shadow: 0 0 #0000;
447 | --tw-shadow-colored: 0 0 #0000;
448 | --tw-blur: ;
449 | --tw-brightness: ;
450 | --tw-contrast: ;
451 | --tw-grayscale: ;
452 | --tw-hue-rotate: ;
453 | --tw-invert: ;
454 | --tw-saturate: ;
455 | --tw-sepia: ;
456 | --tw-drop-shadow: ;
457 | --tw-backdrop-blur: ;
458 | --tw-backdrop-brightness: ;
459 | --tw-backdrop-contrast: ;
460 | --tw-backdrop-grayscale: ;
461 | --tw-backdrop-hue-rotate: ;
462 | --tw-backdrop-invert: ;
463 | --tw-backdrop-opacity: ;
464 | --tw-backdrop-saturate: ;
465 | --tw-backdrop-sepia: ;
466 | }
467 |
468 | ::-webkit-backdrop {
469 | --tw-border-spacing-x: 0;
470 | --tw-border-spacing-y: 0;
471 | --tw-translate-x: 0;
472 | --tw-translate-y: 0;
473 | --tw-rotate: 0;
474 | --tw-skew-x: 0;
475 | --tw-skew-y: 0;
476 | --tw-scale-x: 1;
477 | --tw-scale-y: 1;
478 | --tw-pan-x: ;
479 | --tw-pan-y: ;
480 | --tw-pinch-zoom: ;
481 | --tw-scroll-snap-strictness: proximity;
482 | --tw-ordinal: ;
483 | --tw-slashed-zero: ;
484 | --tw-numeric-figure: ;
485 | --tw-numeric-spacing: ;
486 | --tw-numeric-fraction: ;
487 | --tw-ring-inset: ;
488 | --tw-ring-offset-width: 0px;
489 | --tw-ring-offset-color: #fff;
490 | --tw-ring-color: rgb(59 130 246 / 0.5);
491 | --tw-ring-offset-shadow: 0 0 #0000;
492 | --tw-ring-shadow: 0 0 #0000;
493 | --tw-shadow: 0 0 #0000;
494 | --tw-shadow-colored: 0 0 #0000;
495 | --tw-blur: ;
496 | --tw-brightness: ;
497 | --tw-contrast: ;
498 | --tw-grayscale: ;
499 | --tw-hue-rotate: ;
500 | --tw-invert: ;
501 | --tw-saturate: ;
502 | --tw-sepia: ;
503 | --tw-drop-shadow: ;
504 | --tw-backdrop-blur: ;
505 | --tw-backdrop-brightness: ;
506 | --tw-backdrop-contrast: ;
507 | --tw-backdrop-grayscale: ;
508 | --tw-backdrop-hue-rotate: ;
509 | --tw-backdrop-invert: ;
510 | --tw-backdrop-opacity: ;
511 | --tw-backdrop-saturate: ;
512 | --tw-backdrop-sepia: ;
513 | }
514 |
515 | ::backdrop {
516 | --tw-border-spacing-x: 0;
517 | --tw-border-spacing-y: 0;
518 | --tw-translate-x: 0;
519 | --tw-translate-y: 0;
520 | --tw-rotate: 0;
521 | --tw-skew-x: 0;
522 | --tw-skew-y: 0;
523 | --tw-scale-x: 1;
524 | --tw-scale-y: 1;
525 | --tw-pan-x: ;
526 | --tw-pan-y: ;
527 | --tw-pinch-zoom: ;
528 | --tw-scroll-snap-strictness: proximity;
529 | --tw-ordinal: ;
530 | --tw-slashed-zero: ;
531 | --tw-numeric-figure: ;
532 | --tw-numeric-spacing: ;
533 | --tw-numeric-fraction: ;
534 | --tw-ring-inset: ;
535 | --tw-ring-offset-width: 0px;
536 | --tw-ring-offset-color: #fff;
537 | --tw-ring-color: rgb(59 130 246 / 0.5);
538 | --tw-ring-offset-shadow: 0 0 #0000;
539 | --tw-ring-shadow: 0 0 #0000;
540 | --tw-shadow: 0 0 #0000;
541 | --tw-shadow-colored: 0 0 #0000;
542 | --tw-blur: ;
543 | --tw-brightness: ;
544 | --tw-contrast: ;
545 | --tw-grayscale: ;
546 | --tw-hue-rotate: ;
547 | --tw-invert: ;
548 | --tw-saturate: ;
549 | --tw-sepia: ;
550 | --tw-drop-shadow: ;
551 | --tw-backdrop-blur: ;
552 | --tw-backdrop-brightness: ;
553 | --tw-backdrop-contrast: ;
554 | --tw-backdrop-grayscale: ;
555 | --tw-backdrop-hue-rotate: ;
556 | --tw-backdrop-invert: ;
557 | --tw-backdrop-opacity: ;
558 | --tw-backdrop-saturate: ;
559 | --tw-backdrop-sepia: ;
560 | }
561 |
562 | .ml-2 {
563 | margin-left: 0.5rem;
564 | }
565 |
566 | .ml-4 {
567 | margin-left: 1rem;
568 | }
569 |
570 | .ml-12 {
571 | margin-left: 3rem;
572 | }
573 |
574 | .flex {
575 | display: flex;
576 | }
577 |
578 | .items-center {
579 | align-items: center;
580 | }
581 |
582 | .justify-center {
583 | justify-content: center;
584 | }
585 |
586 | .justify-between {
587 | justify-content: space-between;
588 | }
589 |
590 | .justify-around {
591 | justify-content: space-around;
592 | }
593 |
594 | .rounded-md {
595 | border-radius: 0.375rem;
596 | }
597 |
598 | .rounded-lg {
599 | border-radius: 0.5rem;
600 | }
601 |
602 | .border-\[\#A259FF\] {
603 | --tw-border-opacity: 1;
604 | border-color: rgb(162 89 255 / var(--tw-border-opacity));
605 | }
606 |
607 | .bg-white {
608 | --tw-bg-opacity: 1;
609 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
610 | }
611 |
612 | .bg-\[\#\] {
613 | background-color: #;
614 | }
615 |
616 | .bg-\[\#A259FF\] {
617 | --tw-bg-opacity: 1;
618 | background-color: rgb(162 89 255 / var(--tw-bg-opacity));
619 | }
620 |
621 | .p-4 {
622 | padding: 1rem;
623 | }
624 |
625 | .p-6 {
626 | padding: 1.5rem;
627 | }
628 |
629 | .px-4 {
630 | padding-left: 1rem;
631 | padding-right: 1rem;
632 | }
633 |
634 | .py-1 {
635 | padding-top: 0.25rem;
636 | padding-bottom: 0.25rem;
637 | }
638 |
639 | .pl-2 {
640 | padding-left: 0.5rem;
641 | }
642 |
643 | .pl-6 {
644 | padding-left: 1.5rem;
645 | }
646 |
647 | .pl-12 {
648 | padding-left: 3rem;
649 | }
650 |
651 | .font-\[\'Open_Sans\'\] {
652 | font-family: 'Open Sans';
653 | }
654 |
655 | .font-\[\'\'\] {
656 | font-family: '';
657 | }
658 |
659 | .font-\[\'Space_Mono\'\] {
660 | font-family: 'Space Mono';
661 | }
662 |
663 | .font-\[\'Work_Sans\'\] {
664 | font-family: 'Work Sans';
665 | }
666 |
667 | .text-2xl {
668 | font-size: 1.5rem;
669 | line-height: 2rem;
670 | }
671 |
672 | .text-xl {
673 | font-size: 1.25rem;
674 | line-height: 1.75rem;
675 | }
676 |
677 | .text-white {
678 | --tw-text-opacity: 1;
679 | color: rgb(255 255 255 / var(--tw-text-opacity));
680 | }
681 |
682 | body {
683 | background-color: #2B2B2B;
684 | }
685 |
686 | .hover\:border-2:hover {
687 | border-width: 2px;
688 | }
689 |
690 | .hover\:border:hover {
691 | border-width: 1px;
692 | }
693 |
694 | .hover\:bg-transparent:hover {
695 | background-color: transparent;
696 | }
697 |
698 | .hover\:bg-none:hover {
699 | background-image: none;
700 | }
701 |
--------------------------------------------------------------------------------