├── .gitignore ├── README.md ├── backend ├── .gitignore ├── .sequelizerc ├── README.md ├── app.js ├── bin │ └── www ├── config │ ├── database.js │ └── index.js ├── db │ ├── migrations │ │ ├── 20220825220833-create-user.js │ │ ├── 20220829224626-create-spot.js │ │ ├── 20220829232518-create-booking.js │ │ ├── 20220829234315-create-review.js │ │ ├── 20220829234400-create-review-image.js │ │ └── 20220829234454-create-spot-image.js │ ├── models │ │ ├── booking.js │ │ ├── index.js │ │ ├── review.js │ │ ├── reviewimage.js │ │ ├── spot.js │ │ ├── spotimage.js │ │ └── user.js │ └── seeders │ │ ├── 20220825223406-demo-user.js │ │ ├── 20220830002952-demo-spot.js │ │ ├── 20220923044211-demo-spot-image.js │ │ └── 20220926101531-Reviews.js ├── package-lock.json ├── package.json ├── routes │ ├── api │ │ ├── bookings.js │ │ ├── index.js │ │ ├── maps.js │ │ ├── review-images.js │ │ ├── reviews.js │ │ ├── session.js │ │ ├── spot-images.js │ │ ├── spots.js │ │ ├── testingAuthMware.js │ │ └── users.js │ └── index.js └── utils │ ├── auth.js │ └── validation.js ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favIcon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── site.webmanifest │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── assets │ ├── airnbn.png │ ├── globe.svg │ ├── logo.png │ ├── readMeLogo.svg │ └── up-arrow-svgrepo-com (1).svg │ ├── components │ ├── AllReviews │ │ ├── AllReviews.css │ │ └── index.js │ ├── AllSpots │ │ ├── allSpots.css │ │ └── index.js │ ├── CreateReviewForm │ │ ├── CreateReviewForm.css │ │ └── index.js │ ├── CreateSpotForm │ │ ├── CreateSpotForm.css │ │ ├── countries.js │ │ └── index.js │ ├── Footer │ │ ├── Footer.js │ │ └── footer.css │ ├── GetCurrentReviews │ │ ├── GetCurrentReviews.css │ │ └── index.js │ ├── GetCurrentSpots │ │ ├── GetCurrentSpots.css │ │ └── index.js │ ├── GetCurrentUser │ │ ├── GetCurrentUser.css │ │ └── index.js │ ├── LoginFormModal │ │ ├── LoginForm.css │ │ ├── LoginForm.js │ │ └── index.js │ ├── Maps │ │ ├── BigMap.js │ │ ├── SpotMap.js │ │ ├── bigmap.css │ │ └── memoizationMaps_pass.js │ ├── Navigation │ │ ├── Navigation.css │ │ ├── ProfileButton.js │ │ └── index.js │ ├── SignupFormPage │ │ ├── SignupForm.css │ │ └── index.js │ ├── SingleSpot │ │ ├── SingleSpot.css │ │ └── index.js │ ├── SpotDetails │ │ ├── SpotDetails.css │ │ └── index.js │ ├── UpdateReviewForm │ │ ├── UpdateReviewForm.css │ │ └── index.js │ └── UpdateSpot │ │ ├── UpdateSpot.css │ │ └── index.js │ ├── context │ ├── Modal.css │ └── Modal.js │ ├── index.css │ ├── index.js │ └── store │ ├── bookings.js │ ├── csrf.js │ ├── index.js │ ├── maps.js │ ├── reviews.js │ ├── session.js │ └── spots.js ├── package-lock.json ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build 4 | .DS_Store 5 | *.db 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirNbN 2 | ![Alt text](./frontend/src/assets/readMeLogo.svg) 3 | 4 | 5 | 6 | If you haven't guessed it already, AirNbN is a web app inspired by [AirBnB](https://www.airbnb.com/). 7 | AirNbN can be used as online marketplace for lodging, primarily homestays for vacation rentals, and tourism activities. 8 | AirNbN's greatest attraction is the ability to leverage nights at the houses you own and trade them for nights in others' homes, the only requirement being that both owners are in agreement. 9 | 10 | **Check it out [HERE](https://airnbn-api-project.herokuapp.com/)!** 11 | ## Wiki Links 12 | 13 | * [Backend Routes (API Documentation)](https://github.com/Samsuhhh/API-Project/wiki/Backend-Routes---API-Documentation) 14 | * [Database Schema](https://github.com/Samsuhhh/API-Project/wiki/Database-Schema) 15 | * [Feature List](https://github.com/Samsuhhh/API-Project/wiki/Feature-List) 16 | * [Redux Store Shape](https://github.com/Samsuhhh/API-Project/wiki/Redux-State) 17 | 18 | 19 | ## Tech Stack 20 | 21 | ### Frameworks, Platforms, and Libararies: 22 | 23 | ![Javascript](https://img.shields.io/badge/Javascript%20-F7DF1E?style=for-the-badge&logo=Javascript&logoColor=white) 24 | ![REACT](https://img.shields.io/badge/REACT%20-61DAFB?style=for-the-badge&logo=REACT&logoColor=white) 25 | ![EXPRESS](https://img.shields.io/badge/Express%20-000000?style=for-the-badge&logo=REACT&logoColor=white) 26 | ![REDUX](https://img.shields.io/badge/Redux%20-764ABC?style=for-the-badge&logo=Redux&logoColor=white) 27 | ![Node.js](https://img.shields.io/badge/Node.Js%20-339933?style=for-the-badge&logo=Node.js&logoColor=white) 28 | ![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=HTML5&logoColor=white) 29 | ![CSS3](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=CSS3&logoColor=white) 30 | 31 | 32 | ### Database and Host: 33 | ![Postgresql](https://img.shields.io/badge/Postgresql-4169E1?style=for-the-badge&logo=postgresql&logoColor=white) 34 | ![HEROKU](https://img.shields.io/badge/Heroku-430098?style=for-the-badge&logo=Heroku&logoColor=white) 35 | 36 | ## Get Started 37 | 38 | * Clone the Repo 39 | * run npm install to install dependencies 40 | * cd into frontend and backend/src folders and run npm start in both. 41 | * This will open up our site. Happy Exploring! 42 | 43 | 44 | ### Site Functionality 45 | Here is some functionality to airnbn-api-project.herokuapp.com 46 | 47 | 48 | * This is how a visitor can explore the site without creating account, using a demo login. 49 | (Landing Page) 50 | ![img1](https://i.imgur.com/D4pvduv.gif) 51 | 52 | * This is how you create a review for a spot. 53 | ![img2](https://i.imgur.com/2Qj4lpf.gif) 54 | 55 | * This is how you become a host at airnbn! 56 | ![img3](https://i.imgur.com/qecOifr.gif) 57 | 58 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | build 4 | .DS_Store 5 | *.db 6 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | // backend/.sequelizerc 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | config: path.resolve('config', 'database.js'), 6 | 'models-path': path.resolve('db', 'models'), 7 | 'seeders-path': path.resolve('db', 'seeders'), 8 | 'migrations-path': path.resolve('db', 'migrations') 9 | }; -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | require('express-async-errors'); 3 | const morgan = require('morgan'); 4 | const cors = require('cors'); 5 | const csurf = require('csurf'); 6 | const helmet = require('helmet'); 7 | const cookieParser = require('cookie-parser'); 8 | 9 | const { environment } = require('./config'); 10 | const isProduction = environment === 'production'; 11 | // backend/app.js 12 | 13 | const routes = require('./routes') 14 | const app = express(); 15 | app.use(morgan('dev')); 16 | 17 | app.use(cookieParser()); 18 | app.use(express.json()); 19 | 20 | // Security Middleware 21 | if (!isProduction) { 22 | // enable cors only in development 23 | app.use(cors()); 24 | } 25 | 26 | // helmet helps set a variety of headers to better secure your app 27 | app.use( 28 | helmet.crossOriginResourcePolicy({ 29 | policy: "cross-origin" 30 | }) 31 | ); 32 | 33 | 34 | // Set the _csrf token and create req.csrfToken method 35 | app.use( 36 | csurf({ 37 | cookie: { 38 | secure: isProduction, 39 | sameSite: isProduction && "Lax", 40 | httpOnly: true 41 | } 42 | }) 43 | ); 44 | 45 | // THIS MUST BE USED BEFORE ERROR HANDLERS 46 | app.use(routes); // Connect all the routes 47 | 48 | 49 | // Catch unhandled requests and forward to error handler. 50 | app.use((_req, _res, next) => { 51 | const err = new Error("The requested resource couldn't be found."); 52 | err.title = "Resource Not Found"; 53 | err.errors = ["The requested resource couldn't be found."]; 54 | err.status = 404; 55 | next(err); 56 | }); 57 | 58 | const { ValidationError } = require('sequelize'); 59 | 60 | 61 | // Process sequelize errors 62 | app.use((err, _req, _res, next) => { 63 | // check if error is a Sequelize error: 64 | if (err instanceof ValidationError) { 65 | err.errors = err.errors.map((e) => e.message); 66 | err.title = 'Validation error'; 67 | } 68 | next(err); 69 | }); 70 | 71 | // Error formatter 72 | app.use((err, _req, res, _next) => { 73 | res.status(err.status || 500); 74 | console.error(err); 75 | res.json({ 76 | title: err.title || 'Server Error', 77 | message: err.message, 78 | errors: err.errors, 79 | stack: isProduction ? null : err.stack 80 | }); 81 | }); 82 | 83 | 84 | 85 | 86 | 87 | module.exports = app; -------------------------------------------------------------------------------- /backend/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // backend/bin/www 3 | 4 | // Import environment variables 5 | require('dotenv').config(); 6 | 7 | const { port } = require('../config'); 8 | 9 | const app = require('../app'); 10 | const db = require('../db/models'); 11 | 12 | // Check the database connection before starting the app 13 | db.sequelize 14 | .authenticate() 15 | .then(() => { 16 | console.log('Database connection success! :) Sequelize is ready to use...'); 17 | 18 | // Start listening for connections 19 | app.listen(port, () => console.log(`Listening on port ${port}...`)); 20 | }) 21 | .catch((err) => { 22 | console.log('Database connection failure :('); 23 | console.error(err); 24 | }); -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | // backend/config/database.js 2 | const config = require('./index'); 3 | 4 | module.exports = { 5 | development: { 6 | storage: config.dbFile, 7 | dialect: "sqlite", 8 | seederStorage: "sequelize", 9 | logQueryParameters: true, 10 | typeValidation: true 11 | }, 12 | production: { 13 | use_env_variable: 'DATABASE_URL', 14 | dialect: 'postgres', 15 | seederStorage: 'sequelize', 16 | dialectOptions: { 17 | ssl: { 18 | require: true, 19 | rejectUnauthorized: false 20 | } 21 | } 22 | } 23 | }; -------------------------------------------------------------------------------- /backend/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | // backend/config/index.js 3 | module.exports = { 4 | environment: process.env.NODE_ENV || 'development', 5 | port: process.env.PORT || 8000, 6 | dbFile: process.env.DB_FILE, 7 | jwtConfig: { 8 | secret: process.env.JWT_SECRET, 9 | expiresIn: process.env.JWT_EXPIRES_IN 10 | }, 11 | googleMapsAPIKey: process.env.MAPS_API_KEY 12 | }; 13 | -------------------------------------------------------------------------------- /backend/db/migrations/20220825220833-create-user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | return queryInterface.createTable("Users", { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | firstName: { 12 | type: Sequelize.STRING(25), 13 | allowNull: false 14 | }, 15 | lastName: { 16 | type: Sequelize.STRING(25), 17 | allowNull: false 18 | }, 19 | username: { 20 | type: Sequelize.STRING(30), 21 | allowNull: false, 22 | unique: true 23 | }, 24 | email: { 25 | type: Sequelize.STRING(256), 26 | allowNull: false, 27 | unique: true 28 | }, 29 | hashedPassword: { 30 | type: Sequelize.STRING.BINARY, 31 | allowNull: false 32 | }, 33 | createdAt: { 34 | allowNull: false, 35 | type: Sequelize.DATE, 36 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 37 | }, 38 | updatedAt: { 39 | allowNull: false, 40 | type: Sequelize.DATE, 41 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 42 | } 43 | }); 44 | }, 45 | async down(queryInterface, Sequelize) { 46 | await queryInterface.dropTable('Users'); 47 | } 48 | }; -------------------------------------------------------------------------------- /backend/db/migrations/20220829224626-create-spot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.createTable('Spots', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | onDelete: 'CASCADE' 11 | }, 12 | ownerId: { 13 | type: Sequelize.INTEGER, 14 | onDelete: 'CASCADE', 15 | references: {model: 'Users'} 16 | }, 17 | address: { 18 | type: Sequelize.STRING, 19 | allowNull: false, 20 | }, 21 | city: { 22 | type: Sequelize.STRING, 23 | allowNull: false 24 | }, 25 | state: { 26 | type: Sequelize.STRING, 27 | allowNull: false 28 | }, 29 | country: { 30 | type: Sequelize.STRING, 31 | allowNull: false 32 | }, 33 | lat: { 34 | type: Sequelize.DECIMAL 35 | }, 36 | lng: { 37 | type: Sequelize.DECIMAL 38 | }, 39 | name: { 40 | type: Sequelize.STRING, 41 | allowNull: false, 42 | // unique: true 43 | }, 44 | description: { 45 | type: Sequelize.STRING, 46 | allowNull: false 47 | }, 48 | price: { 49 | type: Sequelize.DECIMAL, 50 | allowNull: false 51 | }, 52 | createdAt: { 53 | allowNull: false, 54 | type: Sequelize.DATE, 55 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 56 | }, 57 | updatedAt: { 58 | allowNull: false, 59 | type: Sequelize.DATE, 60 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 61 | } 62 | }); 63 | }, 64 | async down(queryInterface, Sequelize) { 65 | await queryInterface.dropTable('Spots'); 66 | } 67 | }; -------------------------------------------------------------------------------- /backend/db/migrations/20220829232518-create-booking.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.createTable('Bookings', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | spotId: { 12 | type: Sequelize.INTEGER, 13 | references: { model: 'Spots'}, 14 | onDelete: 'CASCADE' 15 | }, 16 | userId: { 17 | type: Sequelize.INTEGER, 18 | references: { model: 'Users'}, 19 | onDelete: 'CASCADE' 20 | }, 21 | startDate: { 22 | type: Sequelize.DATEONLY 23 | }, 24 | endDate: { 25 | type: Sequelize.DATEONLY 26 | }, 27 | createdAt: { 28 | allowNull: false, 29 | type: Sequelize.DATE, 30 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: Sequelize.DATE, 35 | defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') 36 | } 37 | }); 38 | }, 39 | async down(queryInterface, Sequelize) { 40 | await queryInterface.dropTable('Bookings'); 41 | } 42 | }; -------------------------------------------------------------------------------- /backend/db/migrations/20220829234315-create-review.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.createTable('Reviews', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | spotId: { 12 | type: Sequelize.INTEGER, 13 | references: { model: 'Spots'}, 14 | onDelete: 'CASCADE' 15 | }, 16 | userId: { 17 | type: Sequelize.INTEGER, 18 | references: { model: 'Users'}, 19 | onDelete: 'CASCADE' 20 | }, 21 | review: { 22 | type: Sequelize.STRING 23 | }, 24 | stars: { 25 | type: Sequelize.INTEGER 26 | }, 27 | createdAt: { 28 | allowNull: false, 29 | type: Sequelize.DATE 30 | }, 31 | updatedAt: { 32 | allowNull: false, 33 | type: Sequelize.DATE 34 | } 35 | }); 36 | }, 37 | async down(queryInterface, Sequelize) { 38 | await queryInterface.dropTable('Reviews'); 39 | } 40 | }; -------------------------------------------------------------------------------- /backend/db/migrations/20220829234400-create-review-image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.createTable('ReviewImages', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | reviewId: { 12 | type: Sequelize.INTEGER, 13 | references: { model: 'Reviews'}, 14 | onDelete: 'CASCADE' 15 | }, 16 | url: { 17 | type: Sequelize.STRING 18 | }, 19 | createdAt: { 20 | allowNull: false, 21 | type: Sequelize.DATE 22 | }, 23 | updatedAt: { 24 | allowNull: false, 25 | type: Sequelize.DATE 26 | } 27 | }); 28 | }, 29 | async down(queryInterface, Sequelize) { 30 | await queryInterface.dropTable('ReviewImages'); 31 | } 32 | }; -------------------------------------------------------------------------------- /backend/db/migrations/20220829234454-create-spot-image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | async up(queryInterface, Sequelize) { 4 | await queryInterface.createTable('SpotImages', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | spotId: { 12 | type: Sequelize.INTEGER, 13 | references: { model: 'Spots'}, 14 | onDelete: 'CASCADE' 15 | }, 16 | url: { 17 | type: Sequelize.STRING 18 | }, 19 | preview: { 20 | type: Sequelize.BOOLEAN, 21 | defaultValue: false 22 | }, 23 | createdAt: { 24 | allowNull: false, 25 | type: Sequelize.DATE 26 | }, 27 | updatedAt: { 28 | allowNull: false, 29 | type: Sequelize.DATE 30 | } 31 | }); 32 | }, 33 | async down(queryInterface, Sequelize) { 34 | await queryInterface.dropTable('SpotImages'); 35 | } 36 | }; -------------------------------------------------------------------------------- /backend/db/models/booking.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Booking extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | Booking.belongsTo(models.User, {foreignKey: 'userId'}); 15 | Booking.belongsTo(models.Spot, {foreignKey: 'spotId'}); 16 | } 17 | } 18 | Booking.init({ 19 | spotId: DataTypes.INTEGER, 20 | userId: DataTypes.INTEGER, 21 | startDate: DataTypes.DATEONLY, 22 | endDate: DataTypes.DATEONLY 23 | }, { 24 | sequelize, 25 | modelName: 'Booking', 26 | }); 27 | return Booking; 28 | }; -------------------------------------------------------------------------------- /backend/db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = require(__dirname + '/../../config/database.js')[env]; 9 | const db = {}; 10 | 11 | let sequelize; 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | fs 19 | .readdirSync(__dirname) 20 | .filter(file => { 21 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 22 | }) 23 | .forEach(file => { 24 | const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); 25 | db[model.name] = model; 26 | }); 27 | 28 | Object.keys(db).forEach(modelName => { 29 | if (db[modelName].associate) { 30 | db[modelName].associate(db); 31 | } 32 | }); 33 | 34 | db.sequelize = sequelize; 35 | db.Sequelize = Sequelize; 36 | 37 | module.exports = db; 38 | -------------------------------------------------------------------------------- /backend/db/models/review.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Review extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | Review.hasMany(models.ReviewImage, {foreignKey: 'reviewId'}); 15 | Review.belongsTo(models.User, {foreignKey: 'userId'}); 16 | Review.belongsTo(models.Spot, {foreignKey: 'spotId'}); 17 | } 18 | } 19 | Review.init({ 20 | spotId: DataTypes.INTEGER, 21 | userId: DataTypes.INTEGER, 22 | review: DataTypes.STRING, 23 | stars: DataTypes.INTEGER 24 | }, { 25 | sequelize, 26 | modelName: 'Review' 27 | }); 28 | return Review; 29 | }; -------------------------------------------------------------------------------- /backend/db/models/reviewimage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class ReviewImage extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | ReviewImage.belongsTo(models.Review, {foreignKey: 'reviewId'}); 15 | } 16 | } 17 | ReviewImage.init({ 18 | reviewId: DataTypes.INTEGER, 19 | url: DataTypes.STRING 20 | }, { 21 | sequelize, 22 | modelName: 'ReviewImage', 23 | }); 24 | return ReviewImage; 25 | }; -------------------------------------------------------------------------------- /backend/db/models/spot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Spot extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | Spot.belongsTo(models.User, {as: 'Owner', foreignKey: 'ownerId'}); 15 | Spot.hasMany(models.Review, {foreignKey: 'spotId'}); 16 | Spot.hasMany(models.Booking, {foreignKey: 'spotId'}); 17 | Spot.hasMany(models.SpotImage, {foreignKey: 'spotId'}); 18 | 19 | } 20 | } 21 | Spot.init({ 22 | ownerId: DataTypes.INTEGER, 23 | address: { 24 | type: DataTypes.STRING, 25 | allowNull: false, 26 | validate: { 27 | len: [2, 250] 28 | } 29 | }, 30 | city: { 31 | type: DataTypes.STRING, 32 | allowNull: false, 33 | validate: {len: [2, 250]} 34 | }, 35 | state: { 36 | type: DataTypes.STRING, 37 | allowNull: false, 38 | validate: { len: [2, 250] } 39 | }, 40 | country: { 41 | type: DataTypes.STRING, 42 | allowNull: false, 43 | validate: { len: [2, 250] } 44 | }, 45 | lat: { 46 | type: DataTypes.DECIMAL, 47 | validate: { 48 | isDecimal: true 49 | } 50 | }, 51 | lng: { 52 | type: DataTypes.DECIMAL, 53 | validate: { 54 | isDecimal: true 55 | } 56 | }, 57 | name: { 58 | type: DataTypes.STRING, 59 | allowNull: false, 60 | validate: { len: [2, 250] } 61 | }, 62 | description: { 63 | type: DataTypes.STRING, 64 | allowNull: false, 65 | validate: { len: [2, 250] } 66 | }, 67 | price: { 68 | type: DataTypes.DECIMAL, 69 | allowNull: false, 70 | validate: { min: 1 } 71 | }, 72 | }, { 73 | sequelize, 74 | modelName: 'Spot', 75 | }); 76 | return Spot; 77 | }; -------------------------------------------------------------------------------- /backend/db/models/spotimage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class SpotImage extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | SpotImage.belongsTo(models.Spot, {foreignKey: 'spotId'}); 15 | } 16 | } 17 | SpotImage.init({ 18 | spotId: { 19 | type: DataTypes.INTEGER, 20 | onDelete: 'CASCADE' 21 | }, 22 | url: DataTypes.STRING, 23 | preview: { 24 | type: DataTypes.BOOLEAN, 25 | defaultValue: false 26 | } 27 | }, { 28 | sequelize, 29 | modelName: 'SpotImage', 30 | }); 31 | return SpotImage; 32 | }; -------------------------------------------------------------------------------- /backend/db/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { Model, Validator } = require('sequelize'); 3 | const bcrypt = require('bcryptjs'); 4 | 5 | module.exports = (sequelize, DataTypes) => { 6 | class User extends Model { 7 | toSafeObject() { 8 | const { id, username, email, firstName, lastName } = this; // context will be the User instance 9 | return { id, username, email, firstName, lastName }; 10 | } 11 | // checking if ?? 12 | validatePassword(password) { 13 | return bcrypt.compareSync(password, this.hashedPassword.toString()); 14 | } 15 | 16 | static getCurrentUserById(id){ 17 | return User.scope("currentUser").findByPk(id); 18 | } 19 | 20 | // defining static method for login; 21 | static async login({ credential, password }) { 22 | const { Op } = require('sequelize'); 23 | const user = await User.scope('loginUser').findOne({ 24 | where: { 25 | [Op.or]: { 26 | username: credential, 27 | email: credential 28 | } 29 | } 30 | }); 31 | if (user && user.validatePassword(password)) { 32 | return await User.scope('currentUser').findByPk(user.id); 33 | } 34 | } 35 | // defining static method for signup; accepts obj with username, email, pw 36 | // then hash the pw using bcrypt 37 | static async signup({ username, email, password, firstName, lastName }) { 38 | const hashedPassword = bcrypt.hashSync(password); 39 | const user = await User.create({ 40 | firstName, 41 | lastName, 42 | username, 43 | email, 44 | hashedPassword 45 | }); 46 | return await User.scope('currentUser').findByPk(user.id); 47 | } 48 | 49 | static associate(models) { 50 | // define association here 51 | User.hasMany(models.Spot, {foreignKey: 'ownerId'}); 52 | User.hasMany(models.Booking, {foreignKey: 'userId'}); 53 | User.hasMany(models.Review, {foreignKey: 'userId'}) 54 | } 55 | }; 56 | 57 | User.init( 58 | { 59 | firstName:{ 60 | type: DataTypes.STRING, 61 | allowNull: false, 62 | validate: { 63 | len: [1, 20] 64 | } 65 | }, 66 | lastName: { 67 | type: DataTypes.STRING, 68 | allowNull: false, 69 | validate: { 70 | len: [1, 20] 71 | } 72 | }, 73 | username: { 74 | type: DataTypes.STRING, 75 | allowNull: false, 76 | validate: { 77 | len: [4, 30], 78 | isNotEmail(value) { 79 | if (Validator.isEmail(value)) { 80 | throw new Error("Cannot be an email."); 81 | } 82 | }, 83 | // isUnique(value){ 84 | // if(!isUnique) throw new Error('User already exists') 85 | // } 86 | } 87 | }, 88 | email: { 89 | type: DataTypes.STRING, 90 | allowNull: false, 91 | validate: { 92 | len: [3, 256] 93 | } 94 | }, 95 | hashedPassword: { 96 | type: DataTypes.STRING.BINARY, 97 | allowNull: false, 98 | validate: { 99 | len: [60, 60] 100 | } 101 | } 102 | }, 103 | { 104 | sequelize, 105 | modelName: "User", 106 | indexes: [{ 107 | unique: true, 108 | fields: ['username', 'email'] } 109 | ], 110 | defaultScope: { 111 | attributes: { 112 | exclude: ["hashedPassword", "email", "createdAt", "updatedAt"] 113 | } 114 | }, 115 | scopes: { 116 | currentUser: { 117 | attributes: { exclude: ["hashedPassword", "createdAt", "updatedAt"] } 118 | }, 119 | loginUser: { 120 | attributes: { 121 | exclude: ['createdAt', 'updatedAt'] 122 | } 123 | } 124 | } 125 | } 126 | ); 127 | return User; 128 | }; -------------------------------------------------------------------------------- /backend/db/seeders/20220825223406-demo-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const bcrypt = require("bcryptjs"); 3 | 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | return queryInterface.bulkInsert('Users', [ 7 | { 8 | firstName: 'John', 9 | lastName: 'Carrera', 10 | email: 'John@Carrera.com', 11 | username: 'AyCarrera', 12 | hashedPassword: bcrypt.hashSync('password') 13 | }, 14 | { 15 | firstName: 'Jake', 16 | lastName: 'Matillano', 17 | email: 'Jake@Mat.com', 18 | username: 'JakeyJake123', 19 | hashedPassword: bcrypt.hashSync('soundcloud') 20 | }, 21 | { 22 | firstName: 'Gary', 23 | lastName: 'myBoo', 24 | email: 'user2@user.io', 25 | username: 'GaryAndSam4Ever', 26 | hashedPassword: bcrypt.hashSync('iloveSam') 27 | }, 28 | { 29 | firstName: "Karen", 30 | lastName: 'Smith', 31 | email: 'Karen@Smith.com', 32 | username: 'MamaKaren', 33 | hashedPassword: bcrypt.hashSync('password') 34 | }, 35 | { 36 | firstName: 'David', 37 | lastName: 'Rogers', 38 | email: 'ziggy99@demo.com', 39 | username: '9ziggy9', 40 | hashedPassword: bcrypt.hashSync('June2022') 41 | } 42 | ], {}); 43 | }, 44 | 45 | down: async (queryInterface, Sequelize) => { 46 | const Op = Sequelize.Op; 47 | return queryInterface.bulkDelete('Users', { 48 | username: { [Op.in]: ['AyCarrera', 'JakeyJake123', 'GaryAndSam4Ever', '9ziggy9'] } 49 | }, {}); 50 | } 51 | }; -------------------------------------------------------------------------------- /backend/db/seeders/20220830002952-demo-spot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const demoSpots = [ 4 | { 5 | ownerId: 1, 6 | address: "7000 Joe Morgan Wy", 7 | city: "Oakland", 8 | state: "California", 9 | country: "United States", 10 | lat: 37.75, 11 | lng: -122.20, 12 | name: "BIG Tree house", 13 | description: "300 sq ft treehouse with 3 bedrooms. Close to 7 different hiking trails with a gorgeous open wall bed/bath to breathe in the fresh air.", 14 | price: 100.00 15 | }, 16 | { 17 | ownerId: 1, 18 | address: "495 Bonanza Dr", 19 | city: "Tahoe City", 20 | state: "California", 21 | country: "United States", 22 | lat: 39.16, 23 | lng: -120.15, 24 | name: "Cabin Getaway", 25 | description: "Our custom built guesthouse was completed in 2016. Clean and cozy, this space is perfect for your Tahoe getaway. Recently remodeled with brand new furnishings, kitchen, and bathroom, you will have everything you need for a long or short-term stay!", 26 | price: 530.00 27 | }, 28 | { 29 | ownerId: 1, 30 | address: "45 Brightwood Circle", 31 | city: "Danville", 32 | state: "California", 33 | country: "United States", 34 | lat: 37.82, 35 | lng: -121.94, 36 | name: "City of Stars Entire mansion", 37 | description: "Our location is a great destination for sight-seeing and experiencing boujee Hollywood. With a gorgeous view over all of Los Angeles, our location can fit up to 20 guests.", 38 | price: 3230.00 39 | }, 40 | { 41 | ownerId: 2, 42 | address: "Jalan Danau Tamblingan 89", 43 | city: "Bali", 44 | state: "Sanur", 45 | country: "Indonesia", 46 | lat: -8.26, 47 | lng: 115.09, 48 | name: "Kawasan Pariwisata Nusa Dua", 49 | description: 50 | "Escape in Bali is a distinguished unique eco stay hidden in the jungle of Tampaksiring a village with spiritual ambiance & ancient Balinese mythology. A perfect hideaway for all adventurous travelers, nature lovers, spiritual-minded people, backpackers, artists, long-time travelers & eco-enthusiast to have their like-no-others Bali experience. It is truly live up the concept of back-to-nature.", 51 | price: 490.00 52 | }, 53 | { 54 | ownerId: 2, 55 | address: "23-2 Youido-Dong", 56 | city: "Seoul", 57 | state: "Gyeonggi", 58 | country: "South Korea", 59 | lat: 37.12, 60 | lng: 121.12, 61 | name: "Entire traditional home", 62 | description: "Experience the traditions of Korea with a stay at our beautiful countryside location. Close enough to experience the city life, but far enough to be at peace.", 63 | price: 990.00 64 | }, 65 | { 66 | ownerId: 2, 67 | address: "662 Las Vegas Blvd S", 68 | city: "Las Vegas", 69 | state: "Nevada", 70 | country: 'United States', 71 | lat: 36.16, 72 | lng: -115.15, 73 | name: "Viva Vegas penthouse", 74 | description: "This probably won't be the best vacation spot, but if you're looking for a fun time in Sin City, this is the spot for you and yours.", 75 | price: 770.00 76 | }, 77 | 78 | { 79 | ownerId: 3, 80 | address: "511 Ekekela Pl.", 81 | city: "Kahuku", 82 | state: "Hawaii", 83 | country: "United States", 84 | lat: 21.33, 85 | lng: -157.85, 86 | name: "Island Stay", 87 | description: "The Island Stay beachhouse is a one-of-a-kind beatiful location with direct access to the North Shore of Oahu. With endless activities in the city and tons more in nature, come experience the beauty of Hawaii.", 88 | price: 800.00 89 | }, 90 | 91 | { 92 | ownerId: 3, 93 | address: "1501 Fillmore St.", 94 | city: "San Francisco", 95 | state: "California", 96 | country: "United States", 97 | lat: 37.78, 98 | lng: -122.43, 99 | name: "Entire apartment", 100 | description: "Our location is a great destination for sight-seeing and experiencing, The City. There is no judgement here, just enjoy our city and leave with good vibes.", 101 | price: 420.00 102 | }, 103 | { 104 | ownerId: 3, 105 | address: "6551 Del Playa Drive", 106 | city: "Isla Vista", 107 | state: "California", 108 | country: "United States", 109 | lat: 34.40, 110 | lng: -119.86, 111 | name: "Home Sweet Home", 112 | description: "Tau Palace is a magical place for which words cannot even begin to describe. It's an experience you have to FEEL.. There is also a fun beach and amazing sunsets. Come make memories you will never forget.", 113 | price: 6510.00 114 | }, 115 | { 116 | ownerId: 4, 117 | address: "1 Zion Park Blvd", 118 | city: "Springdale", 119 | state: "Utah", 120 | country: "United States", 121 | lat: 37.20, 122 | lng: -112.98, 123 | name: "Nature's Getaway", 124 | description: "Stay at our beautiful location in Zion National Park. It is a unique experience that you will never forget. No smoking. No pets. No Wifi. Bring your own toiletries.", 125 | price: 888.00 126 | }, 127 | 128 | 129 | { 130 | ownerId: 5, 131 | address: "100 Aker Wood West", 132 | city: "London", 133 | state: "England", 134 | country: "United Kingdom", 135 | lat: 51.61, 136 | lng: 0.135, 137 | name: "Pooh's House", 138 | description: "Come experience what it feels like to live like Winnie-the-Pooh. A great and extremely spacious location for animals of all shapes and sizes. Food and Honey included.", 139 | price: 100.00 140 | }, 141 | 142 | { 143 | ownerId: 1, 144 | address: "1230 Ocean Drive ", 145 | city: "Miami Beach", 146 | state: "Florida", 147 | country: "United States", 148 | lat: 25.78, 149 | lng: -80.13, 150 | name: "Miami Vice", 151 | description: "Our location offers a place for people to let go and just dance the night away. It is a vacation from all the stress and weight life puts on us, so just come to have some fun, dance, and laugh.", 152 | price: 300.00 153 | }, 154 | { 155 | ownerId: 2, 156 | address: "12 Fifth Avenue ", 157 | city: "Manhattan", 158 | state: "New York", 159 | country: "United States", 160 | lat: 40.73, 161 | lng: -73.99, 162 | name: "New York New York", 163 | description: "The pictures speak for themselves, but do not do the location justice. Although cold, the best time to visit is in the winter so you can see the snow blanket our wonderful city.", 164 | price: 910.00 165 | }, 166 | { 167 | ownerId: 3, 168 | address: "4 Dufftown Pl", 169 | city: "Perth", 170 | state: "Inveralmond", 171 | country: "United Kingdom", 172 | lat: 56.42, 173 | lng: -3.47, 174 | name: "!Azkaban", 175 | description: "You know what it is. You know you want to come. Book your stay ASAP as reservations are almost booked for the next year! Experience the magic. ", 176 | price: 910.00 177 | }, 178 | 179 | { 180 | ownerId: 4, 181 | address: "Reineveien 46", 182 | city: "8390 Reine", 183 | state: "Reine", 184 | country: "Norway", 185 | lat: 37.56, 186 | lng: 121.23, 187 | name: "The Halls", 188 | description: "One cannot simply purchase their way into our location. You must be vetted and cleared to enter the sacred halls of Valhalla. Only the worthy shall enter.", 189 | price: 9999999.00 190 | } 191 | ] 192 | const { Spot } = require('../models') 193 | 194 | module.exports = { 195 | async up(queryInterface, Sequelize) { 196 | /** 197 | * Add seed commands here. 198 | * 199 | * Example: 200 | * await queryInterface.bulkInsert('People', [{ 201 | * name: 'John Doe', 202 | * isBetaMember: false 203 | * }], {}); 204 | */ 205 | // return Spot.bulkCreate(demoSpots, { validate: true }) 206 | await queryInterface.bulkInsert('Spots', demoSpots) 207 | }, 208 | 209 | async down(queryInterface, Sequelize) { 210 | for (let spotInfo of validSpots) { 211 | await Spot.destroy({ 212 | where: spotInfo 213 | }); 214 | } 215 | /** 216 | * Add commands to revert seed here. 217 | * 218 | * Example: 219 | * await queryInterface.bulkDelete('People', null, {}); 220 | */ 221 | } 222 | }; 223 | -------------------------------------------------------------------------------- /backend/db/seeders/20220926101531-Reviews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const demoReviews = [ 4 | { 5 | userId: 1, 6 | spotId: 6, 7 | review: "Wow you guys weren't kidding, this place was 'fun' to say the least. I don't remember half of it, but it was awesome.", 8 | stars: 5, 9 | createdAt: 'Now', 10 | updatedAt: 'Now' 11 | }, 12 | { 13 | userId: 1, 14 | spotId: 8, 15 | review: "The City was an amazing place. It was so cute and I just wanted to stay here forever. I loved the vibes and how accepting everyone was.", 16 | stars: 5, 17 | createdAt: 'Now', 18 | updatedAt: 'Now' 19 | }, 20 | { 21 | userId: 1, 22 | spotId: 4, 23 | review: "What an experience. It felt like I was in another world entirely. It was so relaxing and I just felt so at peace. 10/10", 24 | stars: 5, 25 | createdAt: 'Now', 26 | updatedAt: 'Now' 27 | }, 28 | { 29 | userId: 1, 30 | spotId: 5, 31 | review: "I got to travel across the globe to a foreign land and experience the traditional Korean culture. It felt so awesome to be here and experience this.", 32 | stars: 5, 33 | createdAt: 'Now', 34 | updatedAt: 'Now' 35 | }, 36 | { 37 | userId: 1, 38 | spotId: 10, 39 | review: "Zion was a breathtaking trip. The place was so secluded and being in nature was so beautiful. The location was prepped to perfection and after a long day of hiking and nature-ing, it was a great place to relax and rest up.", 40 | stars: 5, 41 | createdAt: 'Now', 42 | updatedAt: 'Now' 43 | }, 44 | { 45 | userId: 2, 46 | spotId: 2, 47 | review: "I am originally from California, so seeing the snow is foreign to me, yet alone living in it. However, the place was beautiful and I have a greater appreciation for the other seasons I didn't know existed. Great Cabin!", 48 | stars: 5, 49 | createdAt: 'Now', 50 | updatedAt: 'Now' 51 | }, 52 | { 53 | userId: 2, 54 | spotId: 3, 55 | review: "The weather was nice, but I was not a fan of Los Angeles. I heard so many stories of it being so great and HOLLYWOOD! but I was dissapointed by the city. Location was 10/10 but the city was underwhelming.", 56 | stars: 3, 57 | createdAt: 'Now', 58 | updatedAt: 'Now' 59 | }, 60 | { 61 | userId: 2, 62 | spotId: 7, 63 | review: "The SUN and the BEACHES were all we wanted, but we got so so so much more! Such a great location, literally on the beach with lots of surfboards and canoes to take out free of charge. Awesome location 11/10!!", 64 | stars: 5, 65 | createdAt: 'Now', 66 | updatedAt: 'Now' 67 | }, 68 | { 69 | userId: 3, 70 | spotId: 2, 71 | review: "Wow. This place looked like it was ripped straight out of a fairytale. The snow was mesmerizing and made me feel all warm and cuddly inside. The fireplace was a huge bonus. 10/10 recommend.", 72 | stars: 5, 73 | createdAt: 'Now', 74 | updatedAt: 'Now' 75 | }, 76 | { 77 | userId: 3, 78 | spotId: 3, 79 | review: "Wow the pictures do not do this location justice. I have never seen a more beautiful house and it was an honor to stay here for our trip. My friends and I had a blast in the house and an even better time going out into the city at night. AWESOME trip!", 80 | stars: 5, 81 | createdAt: 'Now', 82 | updatedAt: 'Now' 83 | }, 84 | { 85 | userId: 3, 86 | spotId: 10, 87 | review: "What can I say to make you read this review and book this location ASAP. Such a great location for the price and TONS of things to do. Conquer your fears and go on the Angels Landing hike and as many others as you can!", 88 | stars: 5, 89 | createdAt: 'Now', 90 | updatedAt: 'Now' 91 | }, 92 | { 93 | userId: 3, 94 | spotId: 4, 95 | review: "Wow. Bali. An experience like no other. Cannot wait to go back. Owners did a great job preparing the place for us and we felt bad having so much fun! Great place for you and yours to vacay!", 96 | stars: 5, 97 | createdAt: 'Now', 98 | updatedAt: 'Now' 99 | }, 100 | { 101 | userId: 4, 102 | spotId: 4, 103 | review: "I booked this place because it had so many great reviews, but I was not impressed. House was NOT clean when we got there and it was not up to my expectations. If you're going to do your job, do it right!!", 104 | stars: 1, 105 | createdAt: 'Now', 106 | updatedAt: 'Now' 107 | }, 108 | { 109 | userId: 4, 110 | spotId: 1, 111 | review: "I don't even know why I chose to book this location in the first place. It's a TREE. Who in their right mind would book this place. If I wanted to live in a tree, I would have been a monkey. If you're stupid, go ahead and book this tree.", 112 | stars: 1, 113 | createdAt: 'Now', 114 | updatedAt: 'Now' 115 | }, 116 | { 117 | userId: 4, 118 | spotId: 7, 119 | review: "I don't get why there are so many good reviews for this location. It was so loud at night, the waves would NOT stop. The owner did not mention it would be so loud I wouldn't even be able to sleep. I want a refund!!!", 120 | stars: 1, 121 | createdAt: 'Now', 122 | updatedAt: 'Now' 123 | }, 124 | { 125 | userId: 4, 126 | spotId: 11, 127 | review: "I only booked this location because it said free food and honey provided, BUT NONE OF IT WAS VEGAN AND GLUTEN FREE. I WANT A REFUND!! Pooh bear wasn't even there. This is craziness.", 128 | stars: 1, 129 | createdAt: 'Now', 130 | updatedAt: 'Now' 131 | }, 132 | { 133 | userId: 3, 134 | spotId: 6, 135 | review: "I'm never coming back. It wasn't a bad stay at all in terms of the Penthouse, but that was a wild ride. Whatever happened here will hopefully remain here. I cannot in good conscious give this place of Sin 5 stars..", 136 | stars: 4, 137 | createdAt: 'Now', 138 | updatedAt: 'Now' 139 | } 140 | ] 141 | 142 | 143 | 144 | 145 | module.exports = { 146 | async up(queryInterface, Sequelize) { 147 | /** 148 | * Add seed commands here. 149 | * 150 | * Example: 151 | * await queryInterface.bulkInsert('People', [{ 152 | * name: 'John Doe', 153 | * isBetaMember: false 154 | * }], {}); 155 | */ 156 | await queryInterface.bulkInsert('Reviews', demoReviews); 157 | }, 158 | 159 | async down(queryInterface, Sequelize) { 160 | /** 161 | * Add commands to revert seed here. 162 | * 163 | * Example: 164 | * await queryInterface.bulkDelete('People', null, {}); 165 | */ 166 | await queryInterface.bulkDelete('Reviews') 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "bcryptjs": "^2.4.3", 4 | "cookie-parser": "^1.4.6", 5 | "cors": "^2.8.5", 6 | "csurf": "^1.11.0", 7 | "dotenv": "^16.0.1", 8 | "express": "^4.18.1", 9 | "express-async-errors": "^3.1.1", 10 | "express-validator": "^6.14.2", 11 | "helmet": "^5.1.1", 12 | "jsonwebtoken": "^8.5.1", 13 | "morgan": "^1.10.0", 14 | "per-env": "^1.0.2", 15 | "pg": "^8.8.0", 16 | "sequelize": "^6.21.4", 17 | "sequelize-cli": "^6.4.1" 18 | }, 19 | "devDependencies": { 20 | "dotenv-cli": "^6.0.0", 21 | "nodemon": "^2.0.19", 22 | "sqlite3": "^5.0.11" 23 | }, 24 | "scripts": { 25 | "sequelize": "sequelize", 26 | "sequelize-cli": "sequelize-cli", 27 | "start": "per-env", 28 | "start:development": "nodemon ./bin/www", 29 | "start:production": "node ./bin/www" 30 | }, 31 | "name": "backend", 32 | "version": "1.0.0", 33 | "main": "app.js", 34 | "keywords": [], 35 | "author": "", 36 | "license": "ISC", 37 | "description": "" 38 | } 39 | -------------------------------------------------------------------------------- /backend/routes/api/bookings.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { Booking, User, Spot, SpotImage } = require('../../db/models'); 3 | const { requireAuth } = require('../../utils/auth'); 4 | const router = express.Router(); 5 | const sequelize = require('sequelize'); 6 | const booking = require('../../db/models/booking'); 7 | 8 | 9 | // GET All current User's BOOKINGS 10 | router.get('/current', requireAuth, async (req, res) => { 11 | const userId = req.user.id 12 | // const findUser = await User.findByPk(req.user.id) 13 | const myBookings = await Booking.findAll({ 14 | where: { 15 | userId: userId 16 | }, 17 | include: [ 18 | { 19 | model: Spot, 20 | attributes: { 21 | exclude: ['createdAt', 'updatedAt'] 22 | }, 23 | }, 24 | 25 | ], 26 | }); 27 | 28 | 29 | // LAZY LOADED 30 | for (let i = 0; i < myBookings.length; i++) { 31 | let bookingsPreview = myBookings[i].toJSON(); 32 | 33 | let bookingImg = bookingsPreview.Spot.id; 34 | const spotImg = await SpotImage.findOne({ 35 | where: { 36 | spotId: bookingImg, 37 | preview: true 38 | } 39 | }); 40 | 41 | if (spotImg) { 42 | bookingsPreview.Spot.previewImage = spotImg.url 43 | } else { 44 | bookingsPreview.Spot.previewImage = 'No image available :(' 45 | } 46 | myBookings[i] = bookingsPreview; 47 | }; 48 | 49 | 50 | 51 | 52 | return res.json({ Bookings: myBookings }) 53 | }); 54 | 55 | 56 | 57 | 58 | // EDIT a Booking with all conflicting date checks 59 | router.put('/:bookingId', requireAuth, async (req, res) => { 60 | const { bookingId } = req.params; 61 | const { startDate, endDate } = req.body; 62 | const findBooking = await Booking.findByPk(bookingId); 63 | 64 | if (!findBooking) { 65 | return res 66 | .status(404) 67 | .json({ 68 | message: "Booking couldn't be found", 69 | statusCode: res.statusCode 70 | }) 71 | }; 72 | 73 | let currentDate = new Date(); 74 | 75 | if (findBooking.endDate < currentDate) { 76 | return res 77 | .status(403) 78 | .json({ 79 | message: "Past bookings can't be modified", 80 | statusCode: res.statusCode 81 | }); 82 | }; 83 | 84 | const currentBookings = await Booking.findAll({ 85 | raw: true, 86 | where: { spotId: findBooking.spotId }, 87 | 88 | }); 89 | 90 | for (let i = 0; i < currentBookings.length; i++) { 91 | if ( 92 | // Date.parse(startDate) >= Date.parse(currentBookings[i].startDate) && 93 | // Date.parse(endDate) <= Date.parse(currentBookings[i].endDate) || 94 | 95 | // Date.parse(startDate) <= Date.parse(currentBookings[i].startDate) && 96 | // Date.parse(endDate) >= Date.parse(currentBookings[i].endDate) || 97 | 98 | // Date.parse(startDate) >= Date.parse(currentBookings[i].startDate) && 99 | // Date.parse(startDate) <= Date.parse(currentBookings[i].endDate) || 100 | 101 | // Date.parse(endDate) >= Date.parse(currentBookings[i].startDate) && 102 | // Date.parse(endDate) <= Date.parse(currentBookings[i].endDate) 103 | 104 | startDate >= currentBookings[i].startDate && 105 | endDate <= currentBookings[i].endDate || 106 | 107 | startDate <= currentBookings[i].startDate && 108 | endDate >= currentBookings[i].endDate || 109 | 110 | startDate >= currentBookings[i].startDate && 111 | startDate <= currentBookings[i].endDate || 112 | 113 | endDate >= currentBookings[i].startDate && 114 | endDate <= currentBookings[i].endDate 115 | 116 | 117 | 118 | ) { 119 | return res 120 | .status(403) 121 | .json({ 122 | message: "Sorry this spot is already booked for the specified dates", 123 | statusCode: res.statusCode, 124 | errors: { 125 | startDate: "Start date conflicts with an existing booking", 126 | endDate: "End date conflicts with an existing booking" 127 | } 128 | }); 129 | }; 130 | }; 131 | 132 | 133 | try { 134 | await findBooking.update({ 135 | startDate: startDate, 136 | endDate: endDate 137 | }); 138 | 139 | return res.json(findBooking); 140 | 141 | } catch (error) { 142 | return res 143 | .status(400) 144 | .json({ 145 | message: "Validation Error", 146 | statusCode: 400, 147 | errors: { 148 | endDate: "endDate cannot come before startDate" 149 | } 150 | }); 151 | } 152 | }); 153 | 154 | 155 | 156 | 157 | 158 | 159 | // DELETE a Booking 160 | router.delete('/:bookingId', requireAuth, async (req, res) => { 161 | const { bookingId } = req.params; 162 | const findBooking = await Booking.findByPk(bookingId) 163 | 164 | if (!findBooking) { 165 | return res 166 | .status(404) 167 | .json({ 168 | message: "Booking couldn't be found", 169 | statusCode: res.statusCode 170 | }) 171 | }; 172 | 173 | 174 | const newDate = new Date(); 175 | let day = newDate.getDate(); 176 | let month = newDate.getMonth(); 177 | let year = newDate.getFullYear(); 178 | let currentDate = `${year}-${month}-${day}`; 179 | 180 | // cannot delete a booking that has already started 181 | if (findBooking.startDate <= currentDate) { 182 | return res 183 | .status(404) 184 | .json({ 185 | message: "Bookings that have been started can't be deleted", 186 | statusCode: res.statusCode 187 | }) 188 | }; 189 | 190 | await findBooking.destroy(); 191 | 192 | return res.json({ 193 | message: 'Successfully deleted', 194 | statusCode: 200 195 | }) 196 | }); 197 | 198 | 199 | 200 | 201 | 202 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/index.js: -------------------------------------------------------------------------------- 1 | // backend/routes/api/index.js 2 | const router = require("express").Router(); 3 | const bookingsRouter = require('./bookings.js'); 4 | const sessionRouter = require('./session.js'); 5 | const usersRouter = require('./users.js'); 6 | const reviewImagesRouter = require('./review-images.js'); 7 | const spotsRouter = require('./spots.js'); 8 | const spotImagesRouter = require('./spot-images.js'); 9 | const reviewsRouter = require('./reviews.js'); 10 | const { restoreUser } = require("../../utils/auth.js"); 11 | const mapsRouter = require('./maps'); 12 | 13 | // Connect restoreUser middleware to the API router 14 | // If current user session is valid, set req.user to the user in the database 15 | // If current user session is not valid, set req.user to null 16 | router.use(restoreUser); 17 | 18 | router.use('/maps', mapsRouter); 19 | 20 | router.use('/bookings', bookingsRouter); 21 | 22 | router.use('/session', sessionRouter); 23 | 24 | router.use('/users', usersRouter); 25 | 26 | router.use('/review-images', reviewImagesRouter); 27 | 28 | router.use('/spots', spotsRouter); 29 | 30 | router.use('/spot-images', spotImagesRouter); 31 | 32 | router.use('/reviews', reviewsRouter); 33 | 34 | router.post('/test', (req, res) => { 35 | res.json({ requestBody: req.body}); 36 | }); 37 | 38 | 39 | 40 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/maps.js: -------------------------------------------------------------------------------- 1 | // backend/routes/api/maps.js 2 | const router = require('express').Router(); 3 | const { googleMapsAPIKey } = require('../../config'); 4 | 5 | router.post('/key', (req, res) => { 6 | res.json({ googleMapsAPIKey }); 7 | }); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/review-images.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ReviewImage, Review, Spot } = require('../../db/models'); 3 | const { requireAuth } = require('../../utils/auth'); 4 | const router = express.Router(); 5 | const sequelize = require('sequelize'); 6 | 7 | 8 | 9 | router.delete('/:imageId', requireAuth, async (req, res) => { 10 | const { imageId } = req.params; 11 | const deleteImage = await ReviewImage.findByPk(imageId); 12 | 13 | if (!deleteImage) { 14 | return res 15 | .status(404) 16 | .json({ 17 | message: "Review Image couldn't be found", 18 | statusCode: res.statusCode 19 | }); 20 | }; 21 | 22 | deleteImage.destroy(); 23 | res.statusCode = 200 24 | return res.json({ 25 | message: 'Successfully deleted', 26 | statusCode: res.statusCode 27 | }); 28 | 29 | }); 30 | 31 | 32 | 33 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/reviews.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { Spot, SpotImage, Review, ReviewImage, User } = require('../../db/models'); 3 | const { requireAuth } = require('../../utils/auth'); 4 | const router = express.Router(); 5 | 6 | 7 | 8 | //GET ALL REVIEWS OF CURRENT USER 9 | router.get('/current', requireAuth, async (req, res) => { 10 | const userId = req.user.id 11 | 12 | const reviews = await Review.findAll({ 13 | include: [ 14 | { 15 | model: User, as: 'User', 16 | attributes: 17 | { exclude: ['email', 'hashedPassword', 'username', 'createdAt', 'updatedAt'] } 18 | }, 19 | { 20 | model: ReviewImage, as: "ReviewImages", 21 | attributes: 22 | { exclude: ['reviewId', 'createdAt', 'updatedAt'] } 23 | }, 24 | { 25 | model: Spot, as: "Spot", 26 | attributes: { 27 | exclude: ['createdAt', 'updatedAt', 'description'] 28 | }, 29 | include: { 30 | model: SpotImage, 31 | where: { preview: true }, 32 | attributes: ['url'] 33 | } 34 | }, 35 | 36 | ], 37 | where: { 38 | userId: userId 39 | }, 40 | }); 41 | for (let i = 0; i < reviews.length; i++) { 42 | let resPreview = reviews[i].toJSON(); 43 | // console.log(resPreview) 44 | let spotImageUrl = resPreview.Spot.SpotImages[0] 45 | // if (!spotImageUrl) return null; 46 | 47 | if (spotImageUrl) { 48 | resPreview.Spot.previewImage = spotImageUrl.url 49 | } else { 50 | resPreview.Spot.previewImage = 'No Image Available' 51 | } 52 | delete resPreview.Spot.SpotImages; 53 | reviews[i] = resPreview; 54 | }; 55 | // console.log('HELLLOO FROM THE BACKEND',reviews) 56 | 57 | return res.json({ Reviews: reviews }) 58 | }); 59 | 60 | 61 | 62 | //EDIT a REVIEW 63 | 64 | router.put('/:reviewId', requireAuth, async (req, res) => { 65 | const { reviewId } = req.params; 66 | const findReview = await Review.findByPk(reviewId); 67 | const { review, stars } = req.body; 68 | 69 | if (!findReview) { 70 | return res 71 | .status(404) 72 | .json({ 73 | message: "Review couldn't be found", 74 | statusCode: res.statusCode 75 | }) 76 | }; 77 | 78 | 79 | try { 80 | findReview.update({ 81 | review: review, 82 | stars, stars 83 | }); 84 | 85 | res.json( findReview ) 86 | 87 | } catch (error) { 88 | return res 89 | .status(400) 90 | .json({ 91 | "message": "Validation error", 92 | statusCode: res.statusCode, 93 | errors: { 94 | review: "Review text is required", 95 | stars: "Stars must be an integer from 1 to 5", 96 | } 97 | }); 98 | }; 99 | }); 100 | 101 | 102 | 103 | 104 | // create an image for a review 105 | router.post('/:reviewId/images', requireAuth, async (req, res) => { 106 | const { reviewId } = req.params; 107 | const { url } = req.body; 108 | const findReview = await Review.findByPk(reviewId); 109 | const userId = req.user.id; 110 | 111 | if (!findReview) { 112 | return res 113 | .status(404) 114 | .json({ 115 | message: "Review couldn't be found", 116 | statusCode: res.statusCode 117 | }) 118 | }; 119 | 120 | if (findReview.userId !== userId) { 121 | return res 122 | .status(403) 123 | .json({ 124 | message: "Slow down there buckaroooo, this doesn't belong to you!", 125 | statusCode: 403 126 | }) 127 | }; 128 | 129 | const hasImages = await ReviewImage.findAll({ 130 | where: { reviewId: reviewId } 131 | }); 132 | 133 | if (hasImages.length >= 10) { 134 | return res 135 | .status(403) 136 | .json({ 137 | message: "Maximum number of images for this resource was reached", 138 | statusCode: res.statusCode 139 | }) 140 | }; 141 | 142 | const addImage = await ReviewImage.create({ 143 | url, 144 | reviewId: reviewId 145 | }); 146 | 147 | return res.json({ 148 | id: addImage.id, 149 | url: url 150 | }); 151 | }); 152 | 153 | 154 | 155 | 156 | // DELETE a REVIEW 157 | 158 | router.delete('/:reviewId', requireAuth, async (req, res) => { 159 | const { reviewId } = req.params; 160 | const deleteReview = await Review.findByPk(reviewId); 161 | const userId = req.user.id; 162 | 163 | if (!deleteReview) { 164 | return res 165 | .status(404) 166 | .json({ 167 | message: "Review couldn't be found", 168 | statusCode: res.statusCode 169 | }); 170 | }; 171 | 172 | if (deleteReview.userId === userId) { 173 | deleteReview.destroy(); 174 | return res.json({ 175 | message: "Successfully deleted", 176 | statusCode: 200 177 | }); 178 | } else { 179 | return res.json({ 180 | message: "You are not the owner of this Review", 181 | }) 182 | } 183 | }); 184 | 185 | 186 | 187 | 188 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/session.js: -------------------------------------------------------------------------------- 1 | // backend/routes/api/session.js 2 | // create and export an express router 3 | const express = require('express'); 4 | 5 | //phase 4 import 6 | const { setTokenCookie, restoreUser } = require('../../utils/auth'); 7 | const { User } = require('../../db/models'); 8 | const router = express.Router(); 9 | 10 | //phase 5 import 11 | const { check } = require('express-validator'); //used with handleValidationErrors to VALIDATE body of a req 12 | const { handleValidationErrors } = require('../../utils/validation'); 13 | 14 | const { requireAuth } = require('../../utils/auth'); 15 | 16 | 17 | //phase 5; is composed of the check and handleValidationErrors middleware 18 | // checks to see if req.body.credentials and req.body.password are EMPTY 19 | const validateLogin = [ 20 | check('credential') 21 | .exists({ checkFalsy: true }) 22 | .notEmpty() 23 | .withMessage('Please provide a valid email or username.'), 24 | check('password') 25 | .exists({ checkFalsy: true }) 26 | .withMessage('Please provide a password.'), 27 | handleValidationErrors 28 | ]; 29 | 30 | 31 | // Log in route handler 32 | // "Add User login backend endpoint" 33 | router.post('/', validateLogin, async (req, res, next) => { 34 | const { credential, password } = req.body; 35 | 36 | const user = await User.login({ credential, password }); 37 | 38 | if (!user) { 39 | // const err = new Error('Login failed'); 40 | // err.status = 401; 41 | // err.title = 'Login failed'; 42 | // err.errors = ['The provided credentials were invalid.']; 43 | res 44 | .status(401) 45 | .json({ 46 | message: "Invalid Credentials", 47 | statusCode: res.statusCode 48 | }); 49 | 50 | return next(err); 51 | } 52 | 53 | const token = await setTokenCookie(res, user); 54 | 55 | const userObj = user.toJSON(); 56 | userObj.token = token; 57 | 58 | return res.json(userObj); 59 | } 60 | ); 61 | 62 | // Log out route handler 63 | // "Add User logout backend endpoint" 64 | router.delete( 65 | '/', 66 | (_req, res) => { 67 | res.clearCookie('token'); 68 | return res.json({ message: 'success' }); 69 | } 70 | ); 71 | 72 | // Restore session user route handlers 73 | // "Add a backend endpoint to get the current user session" 74 | router.get( 75 | '/', 76 | restoreUser, 77 | (req, res) => { 78 | const { user } = req; 79 | if (user) { 80 | return res.json(user); 81 | } else return res.json(null); 82 | // this was wrong :( 83 | // return res 84 | // .status(401) 85 | // .json({ 86 | // message: "Authentication required", 87 | // statusCode: 401 88 | // }); 89 | } 90 | ); 91 | 92 | 93 | 94 | 95 | 96 | module.exports = router; 97 | -------------------------------------------------------------------------------- /backend/routes/api/spot-images.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { SpotImage } = require('../../db/models'); 3 | const { requireAuth } = require('../../utils/auth'); 4 | const router = express.Router(); 5 | 6 | 7 | // Router is live test 8 | // router.get('/', async (req, res) => { 9 | // res.send('HELLO') 10 | // }) 11 | 12 | router.delete('/:imageId', requireAuth, async (req, res) => { 13 | const {imageId} = req.params; 14 | const deleteImage = await SpotImage.findByPk(imageId); 15 | 16 | if(!deleteImage){ 17 | return res 18 | .status(404) 19 | .json({ 20 | message: "Spot Image couldn't be found", 21 | statusCode: res.statusCode 22 | }); 23 | }; 24 | 25 | await deleteImage.destroy(); 26 | res.statusCode = 200 27 | return res.json({ 28 | message: 'Successfully deleted', 29 | statusCode: res.statusCode 30 | }); 31 | 32 | }); 33 | 34 | 35 | 36 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/api/testingAuthMware.js: -------------------------------------------------------------------------------- 1 | 2 | // // TESTING USER AUTH MIDDLEWARES (DELETE LATER) 3 | // // router.post('/test', function (req, res) { 4 | // // res.json({ requestBody: req.body }); 5 | // // }); 6 | 7 | // // // TEST ROUTER (delete me later?) 8 | // // GET /api/set-token-cookie 9 | // const { setTokenCookie } = require('../../utils/auth.js'); 10 | // const { User } = require('../../db/models'); 11 | // router.get('/set-token-cookie', async (_req, res) => { 12 | // const user = await User.findOne({ 13 | // where: { 14 | // username: 'Demo-lition' 15 | // } 16 | // }); 17 | // setTokenCookie(res, user); 18 | // return res.json({ user }); 19 | // }); 20 | 21 | -------------------------------------------------------------------------------- /backend/routes/api/users.js: -------------------------------------------------------------------------------- 1 | // backend/routes/api/users.js 2 | // Create and export an Express router 3 | const express = require('express'); 4 | 5 | const { setTokenCookie, requireAuth } = require('../../utils/auth'); 6 | const { User } = require('../../db/models'); 7 | 8 | const router = express.Router(); 9 | 10 | //phase 5 import 11 | const { check } = require('express-validator'); 12 | const { handleValidationErrors } = require('../../utils/validation'); 13 | 14 | 15 | // POST /api/users singup route expects req to have username, email, pw w/ 16 | //password of the user being created. ValidateSignup checks and validates these keys 17 | // DO NOT REMOVE YET (as per end of Phase 5) 18 | 19 | const validateSignup = [ 20 | check('firstName') 21 | .exists({checkFalsy: true}) 22 | .withMessage('Please provide your First Name'), 23 | check('lastName') 24 | .exists({ checkFalsy: true }) 25 | .withMessage('Please provide your Last Name'), 26 | check('email') 27 | // checks if req.body.email exists and is an email 28 | .exists({ checkFalsy: true }) 29 | .isEmail() 30 | .withMessage('Please provide a valid email.'), 31 | check('username') 32 | // checks if req.body.username is a min length of 4, and is not an email 33 | .exists({ checkFalsy: true }) 34 | .isLength({ min: 4 }) 35 | .withMessage('Please provide a username with at least 4 characters.'), 36 | check('username') 37 | .not() 38 | .isEmail() 39 | .withMessage('Username cannot be an email.'), 40 | check('password') 41 | // checks if req.body.password is not empty and has a min length of 6 42 | .exists({ checkFalsy: true }) 43 | .isLength({ min: 6 }) 44 | .withMessage('Password must be 6 characters or more.'), 45 | handleValidationErrors 46 | ]; 47 | 48 | 49 | // Sign up route handler 50 | //"Add User signup backend endpoint" 51 | router.post('/', validateSignup, async (req, res) => { 52 | const { email, password, username, firstName, lastName} = req.body; 53 | 54 | const emailExists = await User.findOne({where: {email}}); 55 | const usernameExists = await User.findOne({where: {username}}); 56 | 57 | if ( emailExists ) { 58 | return res 59 | .status(403) 60 | .json({ 61 | message: "User already exists", 62 | statusCode: res.statusCode, 63 | errors: [ "User with that email already exists."] 64 | 65 | }) 66 | }; 67 | 68 | if ( usernameExists ) { 69 | return res 70 | .status(403) 71 | .json({ 72 | message: "User already exists", 73 | statusCode: res.statusCode, 74 | errors: [ "User with that username already exists"] 75 | 76 | }) 77 | } 78 | 79 | 80 | const user = await User.signup({ firstName, lastName, email, username, password }); 81 | 82 | await setTokenCookie(res, user); 83 | 84 | return res.json(user); 85 | } 86 | 87 | // router.post('/', validateSignup, async (req, res) => { 88 | // const { email, password, username, firstName, lastName } = req.body; 89 | 90 | // try { 91 | // const user = await User.signup({ 92 | // firstName, lastName, email, username, password 93 | // }); 94 | // } 95 | // catch { 96 | // res.statusCode = 403; 97 | // res.json({ 98 | // message: 'User already exists', 99 | // statusCode: res.statusCode, 100 | // errors: { 101 | // 'email': 'User with that email already exists', 102 | // 'username': 'User with that username already exists' 103 | // } 104 | // }) 105 | // } 106 | 107 | 108 | 109 | 110 | ); 111 | 112 | 113 | 114 | 115 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | // backend/routes/index.js 2 | const express = require('express'); 3 | const router = express.Router(); 4 | const apiRouter = require('./api'); 5 | 6 | router.use('/api', apiRouter); 7 | 8 | // Static routes 9 | // Serve React build files in production 10 | if (process.env.NODE_ENV === 'production') { 11 | const path = require('path'); 12 | // Serve the frontend's index.html file at the root route 13 | router.get('/', (req, res) => { 14 | res.cookie('XSRF-TOKEN', req.csrfToken()); 15 | res.sendFile( 16 | path.resolve(__dirname, '../../frontend', 'build', 'index.html') 17 | ); 18 | }); 19 | 20 | // Serve the static assets in the frontend's build folder 21 | router.use(express.static(path.resolve("../frontend/build"))); 22 | 23 | // Serve the frontend's index.html file at all other routes NOT starting with /api 24 | router.get(/^(?!\/?api).*/, (req, res) => { 25 | res.cookie('XSRF-TOKEN', req.csrfToken()); 26 | res.sendFile( 27 | path.resolve(__dirname, '../../frontend', 'build', 'index.html') 28 | ); 29 | }); 30 | } 31 | 32 | // Add a XSRF-TOKEN cookie in development 33 | if (process.env.NODE_ENV !== 'production') { 34 | router.get('/api/csrf/restore', (req, res) => { 35 | res.cookie('XSRF-TOKEN', req.csrfToken()); 36 | res.status(201).json({}); 37 | }); 38 | } 39 | 40 | module.exports = router; -------------------------------------------------------------------------------- /backend/utils/auth.js: -------------------------------------------------------------------------------- 1 | // backend/utils/auth.js 2 | const jwt = require('jsonwebtoken'); 3 | const { jwtConfig } = require('../config'); 4 | const { User } = require('../db/models'); 5 | 6 | const { secret, expiresIn } = jwtConfig; 7 | 8 | // SENDS a JWT Cookie 9 | const setTokenCookie = (res, user) => { 10 | // Create the token. 11 | const token = jwt.sign( 12 | { data: user.toSafeObject() }, 13 | secret, 14 | { expiresIn: parseInt(expiresIn) } // 604,800 seconds = 1 week 15 | ); 16 | 17 | const isProduction = process.env.NODE_ENV === "production"; 18 | 19 | // Set the token cookie 20 | res.cookie('token', token, { 21 | maxAge: expiresIn * 1000, // maxAge in milliseconds 22 | httpOnly: true, 23 | secure: isProduction, 24 | sameSite: isProduction && "Lax" 25 | }); 26 | 27 | return token; 28 | }; 29 | 30 | 31 | // Create (auth) middleware function that will restore the session user based on JWT cookie 32 | // Create a middleware function that will verify and parse the JWT's payload and search 33 | // database for a USER with the id matching the payload.id 34 | 35 | // restoreUser middleware will be connected to the API router so that all API 36 | // route handlers will check if there is a current user logged in or not 37 | 38 | const restoreUser = (req, res, next) => { 39 | // token parsed from cookies 40 | const { token } = req.cookies; 41 | req.user = null; 42 | 43 | return jwt.verify(token, secret, null, async (err, jwtPayload) => { 44 | if (err) { 45 | return next(); 46 | } 47 | 48 | try { 49 | const { id } = jwtPayload.data; 50 | req.user = await User.scope('currentUser').findByPk(id); 51 | } catch (e) { 52 | res.clearCookie('token'); 53 | return next(); 54 | } 55 | 56 | if (!req.user) res.clearCookie('token'); 57 | 58 | return next(); 59 | }); 60 | }; 61 | 62 | // If there is no current user, return an error 63 | // prevents users who are not logged in from accessing stage changing routes 64 | const requireAuth = [ 65 | restoreUser, 66 | function (req, _res, next) { 67 | if (req.user) return next(); 68 | 69 | const err = new Error('Unauthorized'); 70 | err.title = 'Unauthorized'; 71 | err.errors = ['Unauthorized']; 72 | err.status = 401; 73 | return next(err); 74 | } 75 | ]; 76 | 77 | 78 | 79 | 80 | module.exports = { setTokenCookie, restoreUser, requireAuth } 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /backend/utils/validation.js: -------------------------------------------------------------------------------- 1 | // backend/utils/validation.js 2 | const { validationResult } = require('express-validator'); 3 | 4 | // middleware for formatting errors from express-validator middleware 5 | // (to customize, see express-validator's documentation) 6 | const handleValidationErrors = (req, _res, next) => { 7 | const validationErrors = validationResult(req); 8 | 9 | if (!validationErrors.isEmpty()) { 10 | const errors = validationErrors 11 | .array() 12 | .map((error) => `${error.msg}`); 13 | 14 | const err = Error('Bad request.'); 15 | err.errors = errors; 16 | err.status = 400; 17 | err.title = 'Bad request.'; 18 | next(err); 19 | } 20 | next(); 21 | }; 22 | 23 | module.exports = { 24 | handleValidationErrors 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 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Create React App Template 3 | 4 | A no-frills template from which to create React + Redux applications with 5 | [Create React App](https://github.com/facebook/create-react-app). 6 | 7 | ```sh 8 | npx create-react-app my-app --template @appacademy/react-redux-v17 --use-npm 9 | ``` 10 | 11 | ## Available Scripts 12 | 13 | In the project directory, you can run: 14 | 15 | ### `npm start` 16 | 17 | Runs the app in the development mode.\ 18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 19 | 20 | The page will reload if you make edits.\ 21 | You will also see any lint errors in the console. 22 | 23 | ### `npm test` 24 | 25 | Launches the test runner in the interactive watch mode.\ 26 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 27 | 28 | ### `npm run build` 29 | 30 | Builds the app for production to the `build` folder.\ 31 | It correctly bundles React in production mode and optimizes the build for the best performance. 32 | 33 | The build is minified and the filenames include the hashes.\ 34 | Your app is ready to be deployed! 35 | 36 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 37 | 38 | ### `npm run eject` 39 | 40 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 41 | 42 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 43 | 44 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 45 | 46 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 47 | 48 | ## Learn More 49 | 50 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 51 | 52 | To learn React, check out the [React documentation](https://reactjs.org/). 53 | 54 | ### Code Splitting 55 | 56 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 57 | 58 | ### Analyzing the Bundle Size 59 | 60 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 61 | 62 | ### Making a Progressive Web App 63 | 64 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 65 | 66 | ### Advanced Configuration 67 | 68 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 69 | 70 | ### Deployment 71 | 72 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 73 | 74 | ### `npm run build` fails to minify 75 | 76 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 77 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-google-maps/api": "^2.17.1", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "date-fns": "^2.29.3", 11 | "js-cookie": "^3.0.1", 12 | "react": "^18.2.0", 13 | "react-date-range": "^1.4.0", 14 | "react-dom": "^18.2.0", 15 | "react-loading-skeleton": "^3.1.0", 16 | "react-redux": "^7.2.8", 17 | "react-router-dom": "^5.3.3", 18 | "react-scripts": "5.0.1", 19 | "redux": "^4.2.0", 20 | "redux-thunk": "^2.4.1" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "redux-logger": "^3.0.6" 45 | }, 46 | "proxy": "http://localhost:8000" 47 | } 48 | -------------------------------------------------------------------------------- /frontend/public/favIcon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/favIcon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/favIcon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/favIcon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favIcon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favIcon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsuhhh/API-Project/b089d99f5ce0abdd14a3f1954ec0f4239b006c57/frontend/public/favIcon/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favIcon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AirNbN 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Template", 3 | "name": "Create React App Template", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | // frontend/src/App.js 2 | import React, { useState, useEffect } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import { Route, Switch, useParams } from "react-router-dom"; 5 | import SignupFormPage from "./components/SignupFormPage"; 6 | import * as sessionActions from "./store/session"; 7 | import Navigation from "./components/Navigation"; 8 | import SpotsBrowser from './components/AllSpots/index' 9 | import SpotDetail from "./components/SpotDetails"; 10 | // import { getSpotDetails } from "./store/spots"; 11 | import CreateSpotForm from "./components/CreateSpotForm"; 12 | import UpdateSpotFormPage from "./components/UpdateSpot"; 13 | import CreateReviewForm from "./components/CreateReviewForm"; 14 | import GetCurrentUser from "./components/GetCurrentUser"; 15 | import Footer from "./components/Footer/Footer"; 16 | 17 | function App() { 18 | const dispatch = useDispatch(); 19 | const [isLoaded, setIsLoaded] = useState(false); 20 | 21 | // const spotDetails = useSelector(state => state.spotDetails) 22 | 23 | useEffect(() => { 24 | dispatch(sessionActions.restoreUser()).then(() => setIsLoaded(true)); 25 | }, [dispatch]); 26 | 27 | 28 | 29 | return ( 30 | <> 31 | 32 | {isLoaded && ( 33 | 34 | 35 | 36 | 37 | 38 | 39 |