├── socialmedia-server ├── firebase.json ├── Readme.md ├── functions │ ├── util │ │ ├── admin.js │ │ ├── config.js │ │ ├── fbAuth.js │ │ └── validators.js │ ├── package.json │ ├── index.js │ ├── handlers │ │ ├── screams.js │ │ └── users.js │ └── firebase-debug.log ├── firebase-debug.log └── package-lock.json ├── Screenshots ├── Readme.md ├── Capture.JPG ├── Capture1.JPG ├── Capture2.JPG ├── Capture3.JPG ├── Capture4.JPG ├── Capture5.JPG ├── Capture6.JPG └── Capture7.JPG ├── socialmedia-client ├── public │ ├── icon.png │ ├── manifest.json │ └── index.html ├── src │ ├── images │ │ ├── icon.png │ │ └── no-img.png │ ├── pages │ │ ├── index.js │ │ ├── home.js │ │ ├── user.js │ │ ├── login.js │ │ └── signup.js │ ├── App.test.js │ ├── util │ │ ├── MyButton.js │ │ ├── AuthRoute.js │ │ ├── ProfileSkeleton.js │ │ ├── theme.js │ │ └── ScreamSkeleton.js │ ├── App.css │ ├── index.js │ ├── redux │ │ ├── store.js │ │ ├── reducers │ │ │ ├── uiReducer.js │ │ │ ├── userReducer.js │ │ │ └── dataReducer.js │ │ ├── types.js │ │ └── actions │ │ │ ├── userActions.js │ │ │ └── dataActions.js │ ├── components │ │ ├── layout │ │ │ ├── Navbar.js │ │ │ └── Notifications.js │ │ ├── scream │ │ │ ├── LikeButton.js │ │ │ ├── DeleteScream.js │ │ │ ├── CommentForm.js │ │ │ ├── Comments.js │ │ │ ├── Scream.js │ │ │ ├── PostScream.js │ │ │ └── ScreamDialog.js │ │ └── profile │ │ │ ├── StaticProfile.js │ │ │ ├── EditDetails.js │ │ │ └── Profile.js │ ├── App.js │ └── serviceWorker.js ├── firebase.json ├── Readme.md └── package.json └── README.md /socialmedia-server/firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Screenshots/Readme.md: -------------------------------------------------------------------------------- 1 | Screenshots of the app 2 | -------------------------------------------------------------------------------- /socialmedia-server/Readme.md: -------------------------------------------------------------------------------- 1 | Backend: Used NodeJS, Express, Axios, Busboy, Cors, Firebase 2 | -------------------------------------------------------------------------------- /Screenshots/Capture.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture.JPG -------------------------------------------------------------------------------- /Screenshots/Capture1.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture1.JPG -------------------------------------------------------------------------------- /Screenshots/Capture2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture2.JPG -------------------------------------------------------------------------------- /Screenshots/Capture3.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture3.JPG -------------------------------------------------------------------------------- /Screenshots/Capture4.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture4.JPG -------------------------------------------------------------------------------- /Screenshots/Capture5.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture5.JPG -------------------------------------------------------------------------------- /Screenshots/Capture6.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture6.JPG -------------------------------------------------------------------------------- /Screenshots/Capture7.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/Screenshots/Capture7.JPG -------------------------------------------------------------------------------- /socialmedia-client/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/socialmedia-client/public/icon.png -------------------------------------------------------------------------------- /socialmedia-client/src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/socialmedia-client/src/images/icon.png -------------------------------------------------------------------------------- /socialmedia-client/src/images/no-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dch133/Social-Media-App/HEAD/socialmedia-client/src/images/no-img.png -------------------------------------------------------------------------------- /socialmedia-server/functions/util/admin.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | 3 | admin.initializeApp(); 4 | 5 | const db = admin.firestore(); 6 | 7 | module.exports = { admin, db }; 8 | -------------------------------------------------------------------------------- /socialmedia-client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import home from "./home"; 2 | import login from "./login"; 3 | import signup from "./signup"; 4 | import user from "./user"; 5 | 6 | export { home, login, signup, user }; 7 | -------------------------------------------------------------------------------- /socialmedia-client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /socialmedia-client/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /socialmedia-client/Readme.md: -------------------------------------------------------------------------------- 1 | This is the frontend side. 2 | Used: React, Redux, and Material-UI 3 | 4 | To run this app from the front-end only: 5 | ## 1: Install packages 6 | 7 | run `npm install` 8 | 9 | ## 2: Run project 10 | 11 | run `npm start` 12 | 13 | ## 3: Open it 14 | 15 | go to [http://localhost:3000](http://localhost:3000) 16 | 17 | -------------------------------------------------------------------------------- /socialmedia-server/functions/util/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apiKey: process.env.API_KEY, 3 | authDomain: process.env.AUTH_DOMAIN, 4 | databaseURL: process.env.DATABASE_URL, 5 | projectId: process.env.PROJECT_ID, 6 | storageBucket: process.env.STORAGE_BUCKET, 7 | messagingSenderId: process.env.MESSAGING_SENDER_ID, 8 | appId: process.env.APP_ID 9 | }; 10 | -------------------------------------------------------------------------------- /socialmedia-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "icon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /socialmedia-client/src/util/MyButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tooltip from '@material-ui/core/Tooltip'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | 6 | export default ({ children, onClick, tip, btnClassName, tipClassName }) => ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /socialmedia-client/src/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | background-color: silver; 7 | } 8 | .container { 9 | margin: 80px auto 0 auto; 10 | max-width: 1200px; 11 | } 12 | .nav-container { 13 | margin: auto; 14 | } 15 | .nav-container svg { 16 | color: #fff; 17 | } 18 | a { 19 | text-decoration: none; 20 | } 21 | -------------------------------------------------------------------------------- /socialmedia-client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | 8 | // If you want your app to work offline and load faster, you can change 9 | // unregister() to register() below. Note this comes with some pitfalls. 10 | // Learn more about service workers: https://bit.ly/CRA-PWA 11 | serviceWorker.unregister(); 12 | -------------------------------------------------------------------------------- /socialmedia-client/src/util/AuthRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const AuthRoute = ({ component: Component, authenticated, ...rest }) => ( 7 | 10 | authenticated === true ? : 11 | } 12 | /> 13 | ); 14 | 15 | const mapStateToProps = (state) => ({ 16 | authenticated: state.user.authenticated 17 | }); 18 | 19 | AuthRoute.propTypes = { 20 | user: PropTypes.object 21 | }; 22 | 23 | export default connect(mapStateToProps)(AuthRoute); 24 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | 4 | import userReducer from './reducers/userReducer'; 5 | import dataReducer from './reducers/dataReducer'; 6 | import uiReducer from './reducers/uiReducer'; 7 | 8 | const initialState = {}; 9 | 10 | const middleware = [thunk]; 11 | 12 | const reducers = combineReducers({ 13 | user: userReducer, 14 | data: dataReducer, 15 | UI: uiReducer 16 | }); 17 | 18 | const composeEnhancers = 19 | typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 20 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) 21 | : compose; 22 | 23 | const enhancer = composeEnhancers(applyMiddleware(...middleware)); 24 | const store = createStore(reducers, initialState, enhancer); 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/reducers/uiReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_ERRORS, 3 | CLEAR_ERRORS, 4 | LOADING_UI, 5 | STOP_LOADING_UI 6 | } from '../types'; 7 | 8 | const initialState = { 9 | loading: false, 10 | errors: null 11 | }; 12 | 13 | export default function(state = initialState, action) { 14 | switch (action.type) { 15 | case SET_ERRORS: 16 | return { 17 | ...state, 18 | loading: false, 19 | errors: action.payload 20 | }; 21 | case CLEAR_ERRORS: 22 | return { 23 | ...state, 24 | loading: false, 25 | errors: null 26 | }; 27 | case LOADING_UI: 28 | return { 29 | ...state, 30 | loading: true 31 | }; 32 | case STOP_LOADING_UI: 33 | return { 34 | ...state, 35 | loading: false 36 | }; 37 | default: 38 | return state; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /socialmedia-server/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "engines": { 12 | "node": "8" 13 | }, 14 | "dependencies": { 15 | "@firebase/app": "^0.4.14", 16 | "@firebase/database": "^0.4.12", 17 | "a": "^2.1.2", 18 | "busboy": "^0.3.1", 19 | "express": "^4.17.1", 20 | "firebase": "^6.4.0", 21 | "firebase-admin": "^8.0.0", 22 | "firebase-functions": "^3.2.0", 23 | "of": "^1.0.0", 24 | "peer": "^0.2.10", 25 | "requires": "^1.0.2" 26 | }, 27 | "devDependencies": { 28 | "firebase-functions-test": "^0.1.6" 29 | }, 30 | "private": true 31 | } 32 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/types.js: -------------------------------------------------------------------------------- 1 | // User reducer types 2 | export const SET_AUTHENTICATED = 'SET_AUTHENTICATED'; 3 | export const SET_UNAUTHENTICATED = 'SET_UNAUTHENTICATED'; 4 | export const SET_USER = 'SET_USER'; 5 | export const LOADING_USER = 'LOADING_USER'; 6 | export const MARK_NOTIFICATIONS_READ = 'MARK_NOTIFICATIONS_READ'; 7 | // UI reducer types 8 | export const SET_ERRORS = 'SET_ERRORS'; 9 | export const LOADING_UI = 'LOADING_UI'; 10 | export const CLEAR_ERRORS = 'CLEAR_ERRORS'; 11 | export const LOADING_DATA = 'LOADING_DATA'; 12 | export const STOP_LOADING_UI = 'STOP_LOADING_UI'; 13 | // Data reducer types 14 | export const SET_SCREAMS = 'SET_SCREAMS'; 15 | export const SET_SCREAM = 'SET_SCREAM'; 16 | export const LIKE_SCREAM = 'LIKE_SCREAM'; 17 | export const UNLIKE_SCREAM = 'UNLIKE_SCREAM'; 18 | export const DELETE_SCREAM = 'DELETE_SCREAM'; 19 | export const POST_SCREAM = 'POST_SCREAM'; 20 | export const SUBMIT_COMMENT = 'SUBMIT_COMMENT'; 21 | -------------------------------------------------------------------------------- /socialmedia-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socialape-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^3.9.3", 7 | "@material-ui/icons": "^3.0.2", 8 | "axios": "^0.21.1", 9 | "dayjs": "^1.8.16", 10 | "jwt-decode": "^2.2.0", 11 | "react": "^16.9.0", 12 | "react-dom": "^16.9.0", 13 | "react-redux": "^6.0.1", 14 | "react-router-dom": "^5.0.1", 15 | "react-scripts": "2.1.8", 16 | "redux": "^4.0.4", 17 | "redux-thunk": "^2.3.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead", 31 | "not ie <= 11", 32 | "not op_mini all" 33 | ], 34 | "proxy": "https://us-central1-socialape-7d0b6.cloudfunctions.net/api" 35 | } 36 | -------------------------------------------------------------------------------- /socialmedia-server/functions/util/fbAuth.js: -------------------------------------------------------------------------------- 1 | const { admin, db } = require('./admin'); 2 | 3 | module.exports = (req, res, next) => { 4 | let idToken; 5 | if ( 6 | req.headers.authorization && 7 | req.headers.authorization.startsWith('Bearer ') 8 | ) { 9 | idToken = req.headers.authorization.split('Bearer ')[1]; 10 | } else { 11 | console.error('No token found'); 12 | return res.status(403).json({ error: 'Unauthorized' }); 13 | } 14 | 15 | admin 16 | .auth() 17 | .verifyIdToken(idToken) 18 | .then((decodedToken) => { 19 | req.user = decodedToken; 20 | return db 21 | .collection('users') 22 | .where('userId', '==', req.user.uid) 23 | .limit(1) 24 | .get(); 25 | }) 26 | .then((data) => { 27 | req.user.handle = data.docs[0].data().handle; 28 | req.user.imageUrl = data.docs[0].data().imageUrl; 29 | return next(); 30 | }) 31 | .catch((err) => { 32 | console.error('Error while verifying token ', err); 33 | return res.status(403).json(err); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Social-Media-App 2 | Full stack, fully-featured social media application using React, Firebase, Redux, Express, and Material-UI. 3 | 4 | Implemented backend REST API server with Node.js and Express and cloud functions on Firebase 5 | 6 | To log in: make any email you want and password longer than 4 characters 7 | 8 | Working Sample Account: userx@email.com, 123456 9 | 10 | Features: 11 | - Make posts, like them and add comments 12 | - user login, sign up, and authentication 13 | - Customize your user profile 14 | - Image uploads 15 | - Notifications 16 | 17 | Website: 18 | - Original from Firebase: https://socialape-7d0b6.firebaseapp.com/ 19 | - Github pages: https://dch133.github.io/Social-Media-App/ 20 | 21 | Edit: Remote version of the app will probably stop working as dependencies get outdated. I don't actively support this app anymore. Feel free to suggest changes if you want. 22 | 23 | ------------------------------------------------- 24 | # Running Frontend side locally: 25 | 26 | To run this app: 27 | ## 1: Install packages 28 | 29 | run `npm install` 30 | 31 | ## 2: Run project 32 | 33 | run `npm start` 34 | 35 | ## 3: Open it 36 | 37 | go to [http://localhost:3000](http://localhost:3000) 38 | -------------------------------------------------------------------------------- /socialmedia-client/src/pages/home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import Scream from '../components/scream/Scream'; 6 | import Profile from '../components/profile/Profile'; 7 | import ScreamSkeleton from '../util/ScreamSkeleton'; 8 | 9 | import { connect } from 'react-redux'; 10 | import { getScreams } from '../redux/actions/dataActions'; 11 | 12 | class home extends Component { 13 | componentDidMount() { 14 | this.props.getScreams(); 15 | } 16 | render() { 17 | const { screams, loading } = this.props.data; 18 | let recentScreamsMarkup = !loading ? ( 19 | screams.map((scream) => ) 20 | ) : ( 21 | 22 | ); 23 | return ( 24 | 25 | 26 | {recentScreamsMarkup} 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | home.propTypes = { 37 | getScreams: PropTypes.func.isRequired, 38 | data: PropTypes.object.isRequired 39 | }; 40 | 41 | const mapStateToProps = (state) => ({ 42 | data: state.data 43 | }); 44 | 45 | export default connect( 46 | mapStateToProps, 47 | { getScreams } 48 | )(home); 49 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_USER, 3 | SET_AUTHENTICATED, 4 | SET_UNAUTHENTICATED, 5 | LOADING_USER, 6 | LIKE_SCREAM, 7 | UNLIKE_SCREAM, 8 | MARK_NOTIFICATIONS_READ 9 | } from '../types'; 10 | 11 | const initialState = { 12 | authenticated: false, 13 | loading: false, 14 | credentials: {}, 15 | likes: [], 16 | notifications: [] 17 | }; 18 | 19 | export default function(state = initialState, action) { 20 | switch (action.type) { 21 | case SET_AUTHENTICATED: 22 | return { 23 | ...state, 24 | authenticated: true 25 | }; 26 | case SET_UNAUTHENTICATED: 27 | return initialState; 28 | case SET_USER: 29 | return { 30 | authenticated: true, 31 | loading: false, 32 | ...action.payload 33 | }; 34 | case LOADING_USER: 35 | return { 36 | ...state, 37 | loading: true 38 | }; 39 | case LIKE_SCREAM: 40 | return { 41 | ...state, 42 | likes: [ 43 | ...state.likes, 44 | { 45 | userHandle: state.credentials.handle, 46 | screamId: action.payload.screamId 47 | } 48 | ] 49 | }; 50 | case UNLIKE_SCREAM: 51 | return { 52 | ...state, 53 | likes: state.likes.filter( 54 | (like) => like.screamId !== action.payload.screamId 55 | ) 56 | }; 57 | case MARK_NOTIFICATIONS_READ: 58 | state.notifications.forEach((not) => (not.read = true)); 59 | return { 60 | ...state 61 | }; 62 | default: 63 | return state; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /socialmedia-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | Social Media 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/reducers/dataReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SCREAMS, 3 | LIKE_SCREAM, 4 | UNLIKE_SCREAM, 5 | LOADING_DATA, 6 | DELETE_SCREAM, 7 | POST_SCREAM, 8 | SET_SCREAM, 9 | SUBMIT_COMMENT 10 | } from '../types'; 11 | 12 | const initialState = { 13 | screams: [], 14 | scream: {}, 15 | loading: false 16 | }; 17 | 18 | export default function(state = initialState, action) { 19 | switch (action.type) { 20 | case LOADING_DATA: 21 | return { 22 | ...state, 23 | loading: true 24 | }; 25 | case SET_SCREAMS: 26 | return { 27 | ...state, 28 | screams: action.payload, 29 | loading: false 30 | }; 31 | case SET_SCREAM: 32 | return { 33 | ...state, 34 | scream: action.payload 35 | }; 36 | case LIKE_SCREAM: 37 | case UNLIKE_SCREAM: 38 | let index = state.screams.findIndex( 39 | (scream) => scream.screamId === action.payload.screamId 40 | ); 41 | state.screams[index] = action.payload; 42 | if (state.scream.screamId === action.payload.screamId) { 43 | state.scream = action.payload; 44 | } 45 | return { 46 | ...state 47 | }; 48 | case DELETE_SCREAM: 49 | index = state.screams.findIndex( 50 | (scream) => scream.screamId === action.payload 51 | ); 52 | state.screams.splice(index, 1); 53 | return { 54 | ...state 55 | }; 56 | case POST_SCREAM: 57 | return { 58 | ...state, 59 | screams: [action.payload, ...state.screams] 60 | }; 61 | case SUBMIT_COMMENT: 62 | return { 63 | ...state, 64 | scream: { 65 | ...state.scream, 66 | comments: [action.payload, ...state.scream.comments] 67 | } 68 | }; 69 | default: 70 | return state; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import MyButton from '../../util/MyButton'; 6 | import PostScream from '../scream/PostScream'; 7 | import Notifications from './Notifications'; 8 | // MUI stuff 9 | import AppBar from '@material-ui/core/AppBar'; 10 | import Toolbar from '@material-ui/core/Toolbar'; 11 | import Button from '@material-ui/core/Button'; 12 | // Icons 13 | import HomeIcon from '@material-ui/icons/Home'; 14 | 15 | class Navbar extends Component { 16 | render() { 17 | const { authenticated } = this.props; 18 | return ( 19 | 20 | 21 | {authenticated ? ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) : ( 32 | 33 | 36 | 39 | 42 | 43 | )} 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | Navbar.propTypes = { 51 | authenticated: PropTypes.bool.isRequired 52 | }; 53 | 54 | const mapStateToProps = (state) => ({ 55 | authenticated: state.user.authenticated 56 | }); 57 | 58 | export default connect(mapStateToProps)(Navbar); 59 | -------------------------------------------------------------------------------- /socialmedia-server/functions/util/validators.js: -------------------------------------------------------------------------------- 1 | const isEmail = (email) => { 2 | const regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 3 | if (email.match(regEx)) return true; 4 | else return false; 5 | }; 6 | 7 | const isEmpty = (string) => { 8 | if (string.trim() === '') return true; 9 | else return false; 10 | }; 11 | 12 | exports.validateSignupData = (data) => { 13 | let errors = {}; 14 | 15 | if (isEmpty(data.email)) { 16 | errors.email = 'Must not be empty'; 17 | } else if (!isEmail(data.email)) { 18 | errors.email = 'Must be a valid email address'; 19 | } 20 | 21 | if (isEmpty(data.password)) errors.password = 'Must not be empty'; 22 | if (data.password !== data.confirmPassword) 23 | errors.confirmPassword = 'Passwords must match'; 24 | if (isEmpty(data.handle)) errors.handle = 'Must not be empty'; 25 | 26 | return { 27 | errors, 28 | valid: Object.keys(errors).length === 0 ? true : false 29 | }; 30 | }; 31 | 32 | exports.validateLoginData = (data) => { 33 | let errors = {}; 34 | 35 | if (isEmpty(data.email)) errors.email = 'Must not be empty'; 36 | if (isEmpty(data.password)) errors.password = 'Must not be empty'; 37 | 38 | return { 39 | errors, 40 | valid: Object.keys(errors).length === 0 ? true : false 41 | }; 42 | }; 43 | 44 | exports.reduceUserDetails = (data) => { 45 | let userDetails = {}; 46 | 47 | if (!isEmpty(data.bio.trim())) userDetails.bio = data.bio; 48 | if (!isEmpty(data.website.trim())) { 49 | // https://website.com 50 | if (data.website.trim().substring(0, 4) !== 'http') { 51 | userDetails.website = `http://${data.website.trim()}`; 52 | } else userDetails.website = data.website; 53 | } 54 | if (!isEmpty(data.location.trim())) userDetails.location = data.location; 55 | 56 | return userDetails; 57 | }; 58 | -------------------------------------------------------------------------------- /socialmedia-client/src/util/ProfileSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import NoImg from '../images/no-img.png'; 5 | // MUI 6 | import Paper from '@material-ui/core/Paper'; 7 | // Icons 8 | import LocationOn from '@material-ui/icons/LocationOn'; 9 | import LinkIcon from '@material-ui/icons/Link'; 10 | import CalendarToday from '@material-ui/icons/CalendarToday'; 11 | 12 | const styles = (theme) => ({ 13 | ...theme, 14 | handle: { 15 | height: 20, 16 | backgroundColor: theme.palette.primary.main, 17 | width: 60, 18 | margin: '0 auto 7px auto' 19 | }, 20 | fullLine: { 21 | height: 15, 22 | backgroundColor: 'rgba(0,0,0,0.6)', 23 | width: '100%', 24 | marginBottom: 10 25 | }, 26 | halfLine: { 27 | height: 15, 28 | backgroundColor: 'rgba(0,0,0,0.6)', 29 | width: '50%', 30 | marginBottom: 10 31 | } 32 | }); 33 | 34 | const ProfileSkeleton = (props) => { 35 | const { classes } = props; 36 | return ( 37 | 38 |
39 |
40 | profile 41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Location 50 |
51 | https://website.com 52 |
53 | Joined date 54 |
55 |
56 | 57 | ); 58 | }; 59 | 60 | ProfileSkeleton.propTypes = { 61 | classes: PropTypes.object.isRequired 62 | }; 63 | 64 | export default withStyles(styles)(ProfileSkeleton); 65 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/LikeButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import MyButton from '../../util/MyButton'; 3 | import { Link } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | // Icons 6 | import FavoriteIcon from '@material-ui/icons/Favorite'; 7 | import FavoriteBorder from '@material-ui/icons/FavoriteBorder'; 8 | // REdux 9 | import { connect } from 'react-redux'; 10 | import { likeScream, unlikeScream } from '../../redux/actions/dataActions'; 11 | 12 | export class LikeButton extends Component { 13 | likedScream = () => { 14 | if ( 15 | this.props.user.likes && 16 | this.props.user.likes.find( 17 | (like) => like.screamId === this.props.screamId 18 | ) 19 | ) 20 | return true; 21 | else return false; 22 | }; 23 | likeScream = () => { 24 | this.props.likeScream(this.props.screamId); 25 | }; 26 | unlikeScream = () => { 27 | this.props.unlikeScream(this.props.screamId); 28 | }; 29 | render() { 30 | const { authenticated } = this.props.user; 31 | const likeButton = !authenticated ? ( 32 | 33 | 34 | 35 | 36 | 37 | ) : this.likedScream() ? ( 38 | 39 | 40 | 41 | ) : ( 42 | 43 | 44 | 45 | ); 46 | return likeButton; 47 | } 48 | } 49 | 50 | LikeButton.propTypes = { 51 | user: PropTypes.object.isRequired, 52 | screamId: PropTypes.string.isRequired, 53 | likeScream: PropTypes.func.isRequired, 54 | unlikeScream: PropTypes.func.isRequired 55 | }; 56 | 57 | const mapStateToProps = (state) => ({ 58 | user: state.user 59 | }); 60 | 61 | const mapActionsToProps = { 62 | likeScream, 63 | unlikeScream 64 | }; 65 | 66 | export default connect( 67 | mapStateToProps, 68 | mapActionsToProps 69 | )(LikeButton); 70 | -------------------------------------------------------------------------------- /socialmedia-client/src/util/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | palette: { 3 | primary: { 4 | light: '#33c9dc', 5 | main: '#00bcd4', 6 | dark: '#008394', 7 | contrastText: '#fff' 8 | }, 9 | secondary: { 10 | light: '#ff6333', 11 | main: '#ff3d00', 12 | dark: '#b22a00', 13 | contrastText: '#fff' 14 | } 15 | }, 16 | typography: { 17 | useNextVariants: true 18 | }, 19 | form: { 20 | textAlign: 'center' 21 | }, 22 | image: { 23 | margin: '20px auto 20px auto' 24 | }, 25 | pageTitle: { 26 | margin: '10px auto 10px auto' 27 | }, 28 | textField: { 29 | margin: '10px auto 10px auto' 30 | }, 31 | button: { 32 | marginTop: 20, 33 | position: 'relative' 34 | }, 35 | customError: { 36 | color: 'red', 37 | fontSize: '0.8rem', 38 | marginTop: 10 39 | }, 40 | progress: { 41 | position: 'absolute' 42 | }, 43 | invisibleSeparator: { 44 | border: 'none', 45 | margin: 4 46 | }, 47 | visibleSeparator: { 48 | width: '100%', 49 | borderBottom: '1px solid rgba(0,0,0,0.1)', 50 | marginBottom: 20 51 | }, 52 | paper: { 53 | padding: 20 54 | }, 55 | profile: { 56 | '& .image-wrapper': { 57 | textAlign: 'center', 58 | position: 'relative', 59 | '& button': { 60 | position: 'absolute', 61 | top: '80%', 62 | left: '70%' 63 | } 64 | }, 65 | '& .profile-image': { 66 | width: 200, 67 | height: 200, 68 | objectFit: 'cover', 69 | maxWidth: '100%', 70 | borderRadius: '50%' 71 | }, 72 | '& .profile-details': { 73 | whiteSpace: 'pre-line', 74 | overflow: "hidden", 75 | textAlign: 'center', 76 | '& span, svg': { 77 | verticalAlign: 'middle' 78 | }, 79 | '& a': { 80 | color: '#00bcd4' 81 | } 82 | }, 83 | '& hr': { 84 | border: 'none', 85 | margin: '0 0 10px 0' 86 | }, 87 | '& svg.button': { 88 | '&:hover': { 89 | cursor: 'pointer' 90 | } 91 | } 92 | }, 93 | buttons: { 94 | textAlign: 'center', 95 | '& a': { 96 | margin: '20px 10px' 97 | } 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /socialmedia-client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import "./App.css"; 4 | import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider"; 5 | import createMuiTheme from "@material-ui/core/styles/createMuiTheme"; 6 | import jwtDecode from "jwt-decode"; 7 | // Redux 8 | import { Provider } from "react-redux"; 9 | import store from "./redux/store"; 10 | import { SET_AUTHENTICATED } from "./redux/types"; 11 | import { logoutUser, getUserData } from "./redux/actions/userActions"; 12 | // Components 13 | import Navbar from "./components/layout/Navbar"; 14 | import themeObject from "./util/theme"; 15 | import AuthRoute from "./util/AuthRoute"; 16 | // Pages 17 | import { home, login, signup, user } from "./pages"; 18 | 19 | import axios from "axios"; 20 | 21 | const theme = createMuiTheme(themeObject); 22 | 23 | axios.defaults.baseURL = 24 | "https://us-central1-socialape-7d0b6.cloudfunctions.net/api"; 25 | 26 | const token = localStorage.FBIdToken; 27 | if (token) { 28 | const decodedToken = jwtDecode(token); 29 | if (decodedToken.exp * 1000 < Date.now()) { 30 | store.dispatch(logoutUser()); 31 | window.location.href = "/login"; 32 | } else { 33 | store.dispatch({ type: SET_AUTHENTICATED }); 34 | axios.defaults.headers.common["Authorization"] = token; 35 | store.dispatch(getUserData()); 36 | } 37 | } 38 | 39 | class App extends Component { 40 | render() { 41 | return ( 42 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | } 65 | 66 | export default App; 67 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/profile/StaticProfile.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import dayjs from 'dayjs'; 5 | import { Link } from 'react-router-dom'; 6 | // MUI 7 | import MuiLink from '@material-ui/core/Link'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Typography from '@material-ui/core/Typography'; 10 | // Icons 11 | import LocationOn from '@material-ui/icons/LocationOn'; 12 | import LinkIcon from '@material-ui/icons/Link'; 13 | import CalendarToday from '@material-ui/icons/CalendarToday'; 14 | 15 | const styles = (theme) => ({ 16 | ...theme 17 | }); 18 | 19 | const StaticProfile = (props) => { 20 | const { 21 | classes, 22 | profile: { handle, createdAt, imageUrl, bio, website, location } 23 | } = props; 24 | 25 | return ( 26 | 27 |
28 |
29 | profile 30 |
31 |
32 |
33 | 39 | @{handle} 40 | 41 |
42 | {bio && {bio}} 43 |
44 | {location && ( 45 | 46 | {location} 47 |
48 |
49 | )} 50 | {website && ( 51 | 52 | 53 | 54 | {' '} 55 | {website} 56 | 57 |
58 |
59 | )} 60 | {' '} 61 | Joined {dayjs(createdAt).format('MMM YYYY')} 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | StaticProfile.propTypes = { 69 | profile: PropTypes.object.isRequired, 70 | classes: PropTypes.object.isRequired 71 | }; 72 | 73 | export default withStyles(styles)(StaticProfile); 74 | -------------------------------------------------------------------------------- /socialmedia-client/src/util/ScreamSkeleton.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import NoImg from '../images/no-img.png'; 3 | import PropTypes from 'prop-types'; 4 | // MUI 5 | import Card from '@material-ui/core/Card'; 6 | import CardMedia from '@material-ui/core/CardMedia'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | 9 | import withStyles from '@material-ui/core/styles/withStyles'; 10 | 11 | const styles = (theme) => ({ 12 | ...theme, 13 | card: { 14 | display: 'flex', 15 | marginBottom: 20 16 | }, 17 | cardContent: { 18 | width: '100%', 19 | flexDirection: 'column', 20 | padding: 25 21 | }, 22 | cover: { 23 | width: 100, 24 | height:100, 25 | }, 26 | handle: { 27 | width: 60, 28 | height: 18, 29 | backgroundColor: theme.palette.primary.main, 30 | marginBottom: 7 31 | }, 32 | date: { 33 | height: 14, 34 | width: 100, 35 | backgroundColor: 'rgba(0,0,0, 0.3)', 36 | marginBottom: 10 37 | }, 38 | fullLine: { 39 | height: 15, 40 | width: '90%', 41 | backgroundColor: 'rgba(0,0,0, 0.6)', 42 | marginBottom: 10 43 | }, 44 | halfLine: { 45 | height: 15, 46 | width: '50%', 47 | backgroundColor: 'rgba(0,0,0, 0.6)', 48 | marginBottom: 10 49 | } 50 | }); 51 | 52 | const skeletonImageStyle = { 53 | height: 100, 54 | width: 100, 55 | borderRadius: '50%', 56 | objectFit: 'cover', 57 | marginTop: '30%', 58 | marginLeft: '15%' 59 | }; 60 | 61 | const ScreamSkeleton = (props) => { 62 | const { classes } = props; 63 | 64 | const content = Array.from({ length: 5 }).map((item, index) => ( 65 | 66 | {/* Skeleton profile picture */} 67 | 68 | 69 | 70 | {/* Skeleton Post */} 71 | 72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | )); 80 | 81 | return {content}; 82 | }; 83 | 84 | ScreamSkeleton.propTypes = { 85 | classes: PropTypes.object.isRequired 86 | }; 87 | 88 | export default withStyles(styles)(ScreamSkeleton); 89 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/DeleteScream.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import MyButton from '../../util/MyButton'; 5 | 6 | // MUI Stuff 7 | import Button from '@material-ui/core/Button'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogTitle from '@material-ui/core/DialogTitle'; 10 | import DialogActions from '@material-ui/core/DialogActions'; 11 | import DeleteOutline from '@material-ui/icons/DeleteOutline'; 12 | 13 | import { connect } from 'react-redux'; 14 | import { deleteScream } from '../../redux/actions/dataActions'; 15 | 16 | const styles = { 17 | deleteButton: { 18 | position: 'absolute', 19 | left: '90%', 20 | top: '10%' 21 | } 22 | }; 23 | 24 | class DeleteScream extends Component { 25 | state = { 26 | open: false 27 | }; 28 | handleOpen = () => { 29 | this.setState({ open: true }); 30 | }; 31 | handleClose = () => { 32 | this.setState({ open: false }); 33 | }; 34 | deleteScream = () => { 35 | this.props.deleteScream(this.props.screamId); 36 | this.setState({ open: false }); 37 | }; 38 | render() { 39 | const { classes } = this.props; 40 | 41 | return ( 42 | 43 | 48 | 49 | 50 | 56 | 57 | Are you sure you want to delete this post ? 58 | 59 | 60 | 63 | 66 | 67 | 68 | 69 | ); 70 | } 71 | } 72 | 73 | DeleteScream.propTypes = { 74 | deleteScream: PropTypes.func.isRequired, 75 | classes: PropTypes.object.isRequired, 76 | screamId: PropTypes.string.isRequired 77 | }; 78 | 79 | export default connect( 80 | null, 81 | { deleteScream } 82 | )(withStyles(styles)(DeleteScream)); 83 | -------------------------------------------------------------------------------- /socialmedia-client/src/pages/user.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import axios from 'axios'; 4 | import Scream from '../components/scream/Scream'; 5 | import StaticProfile from '../components/profile/StaticProfile'; 6 | import Grid from '@material-ui/core/Grid'; 7 | 8 | import ScreamSkeleton from '../util/ScreamSkeleton'; 9 | import ProfileSkeleton from '../util/ProfileSkeleton'; 10 | 11 | import { connect } from 'react-redux'; 12 | import { getUserData } from '../redux/actions/dataActions'; 13 | 14 | class user extends Component { 15 | state = { 16 | profile: null, 17 | screamIdParam: null 18 | }; 19 | componentDidMount() { 20 | const handle = this.props.match.params.handle; 21 | const screamId = this.props.match.params.screamId; 22 | 23 | if (screamId) this.setState({ screamIdParam: screamId }); 24 | 25 | this.props.getUserData(handle); 26 | axios 27 | .get(`/user/${handle}`) 28 | .then((res) => { 29 | this.setState({ 30 | profile: res.data.user 31 | }); 32 | }) 33 | .catch((err) => console.log(err)); 34 | } 35 | render() { 36 | const { screams, loading } = this.props.data; 37 | const { screamIdParam } = this.state; 38 | 39 | const screamsMarkup = loading ? ( 40 | 41 | ) : screams === null ? ( 42 |

No screams from this user

43 | ) : !screamIdParam ? ( 44 | screams.map((scream) => ) 45 | ) : ( 46 | screams.map((scream) => { 47 | if (scream.screamId !== screamIdParam) 48 | return ; 49 | else return ; 50 | }) 51 | ); 52 | 53 | return ( 54 | 55 | 56 | {screamsMarkup} 57 | 58 | 59 | {this.state.profile === null ? ( 60 | 61 | ) : ( 62 | 63 | )} 64 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | user.propTypes = { 71 | getUserData: PropTypes.func.isRequired, 72 | data: PropTypes.object.isRequired 73 | }; 74 | 75 | const mapStateToProps = (state) => ({ 76 | data: state.data 77 | }); 78 | 79 | export default connect( 80 | mapStateToProps, 81 | { getUserData } 82 | )(user); 83 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/CommentForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | // MUI Stuff 5 | import Button from '@material-ui/core/Button'; 6 | import Grid from '@material-ui/core/Grid'; 7 | import TextField from '@material-ui/core/TextField'; 8 | // Redux stuff 9 | import { connect } from 'react-redux'; 10 | import { submitComment } from '../../redux/actions/dataActions'; 11 | 12 | const styles = (theme) => ({ 13 | ...theme 14 | }); 15 | 16 | class CommentForm extends Component { 17 | state = { 18 | body: '', 19 | errors: {} 20 | }; 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (nextProps.UI.errors) { 24 | this.setState({ errors: nextProps.UI.errors }); 25 | } 26 | if (!nextProps.UI.errors && !nextProps.UI.loading) { 27 | this.setState({ body: '' }); 28 | } 29 | } 30 | 31 | handleChange = (event) => { 32 | this.setState({ [event.target.name]: event.target.value }); 33 | }; 34 | handleSubmit = (event) => { 35 | event.preventDefault(); 36 | this.props.submitComment(this.props.screamId, { body: this.state.body }); 37 | }; 38 | 39 | render() { 40 | const { classes, authenticated } = this.props; 41 | const errors = this.state.errors; 42 | 43 | const commentFormMarkup = authenticated ? ( 44 | 45 |
46 | 57 | 65 | 66 |
67 |
68 | ) : null; 69 | return commentFormMarkup; 70 | } 71 | } 72 | 73 | CommentForm.propTypes = { 74 | submitComment: PropTypes.func.isRequired, 75 | UI: PropTypes.object.isRequired, 76 | classes: PropTypes.object.isRequired, 77 | screamId: PropTypes.string.isRequired, 78 | authenticated: PropTypes.bool.isRequired 79 | }; 80 | 81 | const mapStateToProps = (state) => ({ 82 | UI: state.UI, 83 | authenticated: state.user.authenticated 84 | }); 85 | 86 | export default connect( 87 | mapStateToProps, 88 | { submitComment } 89 | )(withStyles(styles)(CommentForm)); 90 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/Comments.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import { Link } from 'react-router-dom'; 5 | import dayjs from 'dayjs'; 6 | // MUI 7 | import Grid from '@material-ui/core/Grid'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | const styles = (theme) => ({ 11 | ...theme, 12 | commentImage: { 13 | maxWidth: 100, 14 | height: 100, 15 | objectFit: 'cover', 16 | borderRadius: '50%' 17 | }, 18 | commentData: { 19 | marginLeft: 20 20 | } 21 | }); 22 | 23 | class Comments extends Component { 24 | render() { 25 | const { comments, classes } = this.props; 26 | try{ 27 | return ( 28 | 29 | {comments.map((comment, index) => { 30 | const { body, createdAt, userImage, userHandle } = comment; 31 | return ( 32 | 33 | 34 | 35 | 36 | comment 41 | 42 | 43 |
44 | 50 | {userHandle} 51 | 52 | 53 | {dayjs(createdAt).format('h:mm a, MMMM DD YYYY')} 54 | 55 |
56 | {body} 57 |
58 |
59 |
60 |
61 | {index !== comments.length - 1 && ( 62 |
63 | )} 64 |
65 | ); 66 | })} 67 |
68 | ); 69 | } catch(e) { 70 | console.log('error', e); 71 | window.location.reload(); // Only way so far to like on the Scream dialog 72 | } 73 | } 74 | } 75 | 76 | Comments.propTypes = { 77 | comments: PropTypes.array.isRequired 78 | }; 79 | 80 | export default withStyles(styles)(Comments); 81 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_USER, 3 | SET_ERRORS, 4 | CLEAR_ERRORS, 5 | LOADING_UI, 6 | SET_UNAUTHENTICATED, 7 | LOADING_USER, 8 | MARK_NOTIFICATIONS_READ 9 | } from '../types'; 10 | import axios from 'axios'; 11 | 12 | export const loginUser = (userData, history) => (dispatch) => { 13 | dispatch({ type: LOADING_UI }); 14 | axios 15 | .post('/login', userData) 16 | .then((res) => { 17 | setAuthorizationHeader(res.data.token); 18 | dispatch(getUserData()); 19 | dispatch({ type: CLEAR_ERRORS }); 20 | history.push('/'); 21 | }) 22 | .catch((err) => { 23 | dispatch({ 24 | type: SET_ERRORS, 25 | payload: err.response.data 26 | }); 27 | }); 28 | }; 29 | 30 | export const signupUser = (newUserData, history) => (dispatch) => { 31 | dispatch({ type: LOADING_UI }); 32 | axios 33 | .post('/signup', newUserData) 34 | .then((res) => { 35 | setAuthorizationHeader(res.data.token); 36 | dispatch(getUserData()); 37 | dispatch({ type: CLEAR_ERRORS }); 38 | history.push('/'); 39 | }) 40 | .catch((err) => { 41 | dispatch({ 42 | type: SET_ERRORS, 43 | payload: err.response.data 44 | }); 45 | }); 46 | }; 47 | 48 | export const logoutUser = () => (dispatch) => { 49 | localStorage.removeItem('FBIdToken'); 50 | delete axios.defaults.headers.common['Authorization']; 51 | dispatch({ type: SET_UNAUTHENTICATED }); 52 | }; 53 | 54 | export const getUserData = () => (dispatch) => { 55 | dispatch({ type: LOADING_USER }); 56 | axios 57 | .get('/user') 58 | .then((res) => { 59 | dispatch({ 60 | type: SET_USER, 61 | payload: res.data 62 | }); 63 | }) 64 | .catch((err) => console.log(err)); 65 | }; 66 | 67 | export const uploadImage = (formData) => (dispatch) => { 68 | dispatch({ type: LOADING_USER }); 69 | axios 70 | .post('/user/image', formData) 71 | .then(() => { 72 | dispatch(getUserData()); 73 | }) 74 | .catch((err) => console.log(err)); 75 | }; 76 | 77 | export const editUserDetails = (userDetails) => (dispatch) => { 78 | dispatch({ type: LOADING_USER }); 79 | axios 80 | .post('/user', userDetails) 81 | .then(() => { 82 | dispatch(getUserData()); 83 | }) 84 | .catch((err) => console.log(err)); 85 | }; 86 | 87 | export const markNotificationsRead = (notificationIds) => (dispatch) => { 88 | axios 89 | .post('/notifications', notificationIds) 90 | .then((res) => { 91 | dispatch({ 92 | type: MARK_NOTIFICATIONS_READ 93 | }); 94 | }) 95 | .catch((err) => console.log(err)); 96 | }; 97 | 98 | const setAuthorizationHeader = (token) => { 99 | const FBIdToken = `Bearer ${token}`; 100 | localStorage.setItem('FBIdToken', FBIdToken); 101 | axios.defaults.headers.common['Authorization'] = FBIdToken; 102 | }; 103 | -------------------------------------------------------------------------------- /socialmedia-client/src/redux/actions/dataActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SCREAMS, 3 | LOADING_DATA, 4 | LIKE_SCREAM, 5 | UNLIKE_SCREAM, 6 | DELETE_SCREAM, 7 | SET_ERRORS, 8 | POST_SCREAM, 9 | CLEAR_ERRORS, 10 | LOADING_UI, 11 | SET_SCREAM, 12 | STOP_LOADING_UI, 13 | SUBMIT_COMMENT 14 | } from '../types'; 15 | import axios from 'axios'; 16 | 17 | // Get all screams 18 | export const getScreams = () => (dispatch) => { 19 | dispatch({ type: LOADING_DATA }); 20 | axios 21 | .get('/screams') 22 | .then((res) => { 23 | dispatch({ 24 | type: SET_SCREAMS, 25 | payload: res.data 26 | }); 27 | }) 28 | .catch((err) => { 29 | dispatch({ 30 | type: SET_SCREAMS, 31 | payload: [] 32 | }); 33 | }); 34 | }; 35 | export const getScream = (screamId) => (dispatch) => { 36 | dispatch({ type: LOADING_UI }); 37 | axios 38 | .get(`/scream/${screamId}`) 39 | .then((res) => { 40 | dispatch({ 41 | type: SET_SCREAM, 42 | payload: res.data 43 | }); 44 | dispatch({ type: STOP_LOADING_UI }); 45 | }) 46 | .catch((err) => console.log(err)); 47 | }; 48 | // Post a scream 49 | export const postScream = (newScream) => (dispatch) => { 50 | dispatch({ type: LOADING_UI }); 51 | axios 52 | .post('/scream', newScream) 53 | .then((res) => { 54 | dispatch({ 55 | type: POST_SCREAM, 56 | payload: res.data 57 | }); 58 | dispatch(clearErrors()); 59 | }) 60 | .catch((err) => { 61 | dispatch({ 62 | type: SET_ERRORS, 63 | payload: err.response.data 64 | }); 65 | }); 66 | }; 67 | // Like a scream 68 | export const likeScream = (screamId) => (dispatch) => { 69 | axios 70 | .get(`/scream/${screamId}/like`) 71 | .then((res) => { 72 | dispatch({ 73 | type: LIKE_SCREAM, 74 | payload: res.data 75 | }); 76 | }) 77 | .catch((err) => console.log(err)); 78 | }; 79 | // Unlike a scream 80 | export const unlikeScream = (screamId) => (dispatch) => { 81 | axios 82 | .get(`/scream/${screamId}/unlike`) 83 | .then((res) => { 84 | dispatch({ 85 | type: UNLIKE_SCREAM, 86 | payload: res.data 87 | }); 88 | }) 89 | .catch((err) => console.log(err)); 90 | }; 91 | // Submit a comment 92 | export const submitComment = (screamId, commentData) => (dispatch) => { 93 | axios 94 | .post(`/scream/${screamId}/comment`, commentData) 95 | .then((res) => { 96 | dispatch({ 97 | type: SUBMIT_COMMENT, 98 | payload: res.data 99 | }); 100 | dispatch(clearErrors()); 101 | }) 102 | .catch((err) => { 103 | dispatch({ 104 | type: SET_ERRORS, 105 | payload: err.response.data 106 | }); 107 | }); 108 | }; 109 | export const deleteScream = (screamId) => (dispatch) => { 110 | axios 111 | .delete(`/scream/${screamId}`) 112 | .then(() => { 113 | dispatch({ type: DELETE_SCREAM, payload: screamId }); 114 | }) 115 | .catch((err) => console.log(err)); 116 | }; 117 | 118 | export const getUserData = (userHandle) => (dispatch) => { 119 | dispatch({ type: LOADING_DATA }); 120 | axios 121 | .get(`/user/${userHandle}`) 122 | .then((res) => { 123 | dispatch({ 124 | type: SET_SCREAMS, 125 | payload: res.data.screams 126 | }); 127 | }) 128 | .catch(() => { 129 | dispatch({ 130 | type: SET_SCREAMS, 131 | payload: null 132 | }); 133 | }); 134 | }; 135 | 136 | export const clearErrors = () => (dispatch) => { 137 | dispatch({ type: CLEAR_ERRORS }); 138 | }; 139 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/Scream.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import { Link } from 'react-router-dom'; 4 | import dayjs from 'dayjs'; 5 | import relativeTime from 'dayjs/plugin/relativeTime'; 6 | import PropTypes from 'prop-types'; 7 | import MyButton from '../../util/MyButton'; 8 | import DeleteScream from './DeleteScream'; 9 | import ScreamDialog from './ScreamDialog'; 10 | import LikeButton from './LikeButton'; 11 | // MUI Stuff 12 | import Card from '@material-ui/core/Card'; 13 | import CardContent from '@material-ui/core/CardContent'; 14 | import CardMedia from '@material-ui/core/CardMedia'; 15 | import Typography from '@material-ui/core/Typography'; 16 | // Icons 17 | import ChatIcon from '@material-ui/icons/Chat'; 18 | // Redux 19 | import { connect } from 'react-redux'; 20 | 21 | const styles = { 22 | card: { 23 | position: 'relative', 24 | display: 'flex', 25 | marginBottom: 20 26 | }, 27 | image: { 28 | width: 100, 29 | height:100, 30 | }, 31 | content: { 32 | padding: 25, 33 | objectFit: 'cover', 34 | whiteSpace: 'pre-line', 35 | overflow: "hidden", 36 | maxWidth: 600 37 | } 38 | }; 39 | 40 | class Scream extends Component { 41 | render() { 42 | dayjs.extend(relativeTime); 43 | const { 44 | classes, 45 | scream: { 46 | body, 47 | createdAt, 48 | userImage, 49 | userHandle, 50 | screamId, 51 | likeCount, 52 | commentCount 53 | }, 54 | user: { 55 | authenticated, 56 | credentials: { handle } 57 | } 58 | } = this.props; 59 | 60 | const deleteButton = 61 | authenticated && userHandle === handle ? ( 62 | 63 | ) : null; 64 | 65 | const profileImageStyle = { 66 | height: 100, 67 | width: 100, 68 | borderRadius: '50%', 69 | objectFit: 'cover', 70 | marginTop: '30%', 71 | marginLeft: '15%' 72 | }; 73 | 74 | return ( 75 | 76 | 80 | 81 | 82 | 83 | {/* User name */} 84 | 90 | {userHandle} 91 | {/* Delete btn */} 92 | 93 | {deleteButton} 94 | {/* Date of message */} 95 | 96 | {dayjs(createdAt).fromNow()} 97 | 98 | {/* Message */} 99 | {body} 102 | 103 | {/* Like btn */} 104 | 105 | {likeCount} Likes 106 | {/* Comments btn */} 107 | 108 | 109 | 110 | {commentCount} Comments 111 | {/* Expand to view post with comments */} 112 | 117 | 118 | 119 | ); 120 | } 121 | } 122 | 123 | Scream.propTypes = { 124 | user: PropTypes.object.isRequired, 125 | scream: PropTypes.object.isRequired, 126 | classes: PropTypes.object.isRequired, 127 | openDialog: PropTypes.bool 128 | }; 129 | 130 | const mapStateToProps = (state) => ({ 131 | user: state.user 132 | }); 133 | 134 | export default connect(mapStateToProps)(withStyles(styles)(Scream)); 135 | -------------------------------------------------------------------------------- /socialmedia-client/src/pages/login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import AppIcon from '../images/icon.png'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | // MUI Stuff 8 | import Grid from '@material-ui/core/Grid'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Button from '@material-ui/core/Button'; 12 | import CircularProgress from '@material-ui/core/CircularProgress'; 13 | // Redux stuff 14 | import { connect } from 'react-redux'; 15 | import { loginUser } from '../redux/actions/userActions'; 16 | 17 | const styles = (theme) => ({ 18 | ...theme 19 | }); 20 | 21 | class login extends Component { 22 | constructor() { 23 | super(); 24 | this.state = { 25 | email: '', 26 | password: '', 27 | errors: {} 28 | }; 29 | } 30 | componentWillReceiveProps(nextProps) { 31 | if (nextProps.UI.errors) { 32 | this.setState({ errors: nextProps.UI.errors }); 33 | } 34 | } 35 | handleSubmit = (event) => { 36 | event.preventDefault(); 37 | const userData = { 38 | email: this.state.email, 39 | password: this.state.password 40 | }; 41 | this.props.loginUser(userData, this.props.history); 42 | }; 43 | handleChange = (event) => { 44 | this.setState({ 45 | [event.target.name]: event.target.value 46 | }); 47 | }; 48 | render() { 49 | const { 50 | classes, 51 | UI: { loading } 52 | } = this.props; 53 | const { errors } = this.state; 54 | 55 | return ( 56 | 57 | 58 | 59 | app icon 60 | 61 | Login 62 | 63 |
64 | 76 | 88 | {errors.general && ( 89 | 90 | {errors.general} 91 | 92 | )} 93 | 105 |
106 | 107 | Don't have an account ? Sign up here 108 | 109 | 110 |
111 | 112 | 113 | ); 114 | } 115 | } 116 | 117 | login.propTypes = { 118 | classes: PropTypes.object.isRequired, 119 | loginUser: PropTypes.func.isRequired, 120 | user: PropTypes.object.isRequired, 121 | UI: PropTypes.object.isRequired 122 | }; 123 | 124 | const mapStateToProps = (state) => ({ 125 | user: state.user, 126 | UI: state.UI 127 | }); 128 | 129 | const mapActionsToProps = { 130 | loginUser 131 | }; 132 | 133 | export default connect( 134 | mapStateToProps, 135 | mapActionsToProps 136 | )(withStyles(styles)(login)); 137 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/PostScream.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | // MUI Stuff 6 | import Button from '@material-ui/core/Button'; 7 | import TextField from '@material-ui/core/TextField'; 8 | import Dialog from '@material-ui/core/Dialog'; 9 | import DialogContent from '@material-ui/core/DialogContent'; 10 | import DialogTitle from '@material-ui/core/DialogTitle'; 11 | import CircularProgress from '@material-ui/core/CircularProgress'; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import CloseIcon from '@material-ui/icons/Close'; 14 | // Redux stuff 15 | import { connect } from 'react-redux'; 16 | import { postScream, clearErrors } from '../../redux/actions/dataActions'; 17 | 18 | const styles = (theme) => ({ 19 | ...theme, 20 | submitButton: { 21 | position: 'relative', 22 | float: 'right', 23 | marginTop: 10 24 | }, 25 | progressSpinner: { 26 | position: 'absolute' 27 | }, 28 | closeButton: { 29 | position: 'absolute', 30 | left: '91%', 31 | top: '6%' 32 | } 33 | }); 34 | 35 | class PostScream extends Component { 36 | state = { 37 | open: false, 38 | body: '', 39 | errors: {} 40 | }; 41 | componentWillReceiveProps(nextProps) { 42 | if (nextProps.UI.errors) { 43 | this.setState({ 44 | errors: nextProps.UI.errors 45 | }); 46 | } 47 | if (!nextProps.UI.errors && !nextProps.UI.loading) { 48 | this.setState({ body: '', open: false, errors: {} }); 49 | } 50 | } 51 | handleOpen = () => { 52 | this.setState({ open: true }); 53 | }; 54 | handleClose = () => { 55 | this.props.clearErrors(); 56 | this.setState({ open: false, errors: {} }); 57 | }; 58 | handleChange = (event) => { 59 | this.setState({ [event.target.name]: event.target.value }); 60 | }; 61 | handleSubmit = (event) => { 62 | event.preventDefault(); 63 | this.props.postScream({ body: this.state.body }); 64 | }; 65 | render() { 66 | const { errors } = this.state; 67 | const { 68 | classes, 69 | UI: { loading } 70 | } = this.props; 71 | return ( 72 | 73 | 74 | 75 | 76 | 82 | 87 | 88 | 89 | Create Post 90 | 91 |
92 | 105 | 120 | 121 |
122 |
123 |
124 | ); 125 | } 126 | } 127 | 128 | PostScream.propTypes = { 129 | postScream: PropTypes.func.isRequired, 130 | clearErrors: PropTypes.func.isRequired, 131 | UI: PropTypes.object.isRequired 132 | }; 133 | 134 | const mapStateToProps = (state) => ({ 135 | UI: state.UI 136 | }); 137 | 138 | export default connect( 139 | mapStateToProps, 140 | { postScream, clearErrors } 141 | )(withStyles(styles)(PostScream)); 142 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/layout/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import dayjs from 'dayjs'; 4 | import relativeTime from 'dayjs/plugin/relativeTime'; 5 | import PropTypes from 'prop-types'; 6 | // MUI stuff 7 | import Menu from '@material-ui/core/Menu'; 8 | import MenuItem from '@material-ui/core/MenuItem'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import Tooltip from '@material-ui/core/Tooltip'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import Badge from '@material-ui/core/Badge'; 13 | // Icons 14 | import NotificationsIcon from '@material-ui/icons/Notifications'; 15 | import FavoriteIcon from '@material-ui/icons/Favorite'; 16 | import ChatIcon from '@material-ui/icons/Chat'; 17 | // Redux 18 | import { connect } from 'react-redux'; 19 | import { markNotificationsRead } from '../../redux/actions/userActions'; 20 | 21 | class Notifications extends Component { 22 | state = { 23 | anchorEl: null 24 | }; 25 | handleOpen = (event) => { 26 | this.setState({ anchorEl: event.target }); 27 | }; 28 | handleClose = () => { 29 | this.setState({ anchorEl: null }); 30 | }; 31 | onMenuOpened = () => { 32 | let unreadNotificationsIds = this.props.notifications 33 | .filter((not) => !not.read) 34 | .map((not) => not.notificationId); 35 | this.props.markNotificationsRead(unreadNotificationsIds); 36 | }; 37 | render() { 38 | const notifications = this.props.notifications; 39 | const anchorEl = this.state.anchorEl; 40 | 41 | dayjs.extend(relativeTime); 42 | 43 | let notificationsIcon; 44 | if (notifications && notifications.length > 0) { 45 | notifications.filter((not) => not.read === false).length > 0 46 | ? (notificationsIcon = ( 47 | not.read === false).length 50 | } 51 | color="secondary" 52 | > 53 | 54 | 55 | )) 56 | : (notificationsIcon = ); 57 | } else { 58 | notificationsIcon = ; 59 | } 60 | let notificationsMarkup = 61 | notifications && notifications.length > 0 ? ( 62 | notifications.map((not) => { 63 | const verb = not.type === 'like' ? 'liked' : 'commented on'; 64 | const time = dayjs(not.createdAt).fromNow(); 65 | const iconColor = not.read ? 'primary' : 'secondary'; 66 | const icon = 67 | not.type === 'like' ? ( 68 | 69 | ) : ( 70 | 71 | ); 72 | 73 | return ( 74 | 75 | {icon} 76 | 82 | {not.sender} {verb} your post, {time} 83 | 84 | 85 | ); 86 | }) 87 | ) : ( 88 | 89 | You have no notifications yet 90 | 91 | ); 92 | return ( 93 | 94 | 95 | 100 | {notificationsIcon} 101 | 102 | 103 | 109 | {notificationsMarkup} 110 | 111 | 112 | ); 113 | } 114 | } 115 | 116 | Notifications.propTypes = { 117 | markNotificationsRead: PropTypes.func.isRequired, 118 | notifications: PropTypes.array.isRequired 119 | }; 120 | 121 | const mapStateToProps = (state) => ({ 122 | notifications: state.user.notifications 123 | }); 124 | 125 | export default connect( 126 | mapStateToProps, 127 | { markNotificationsRead } 128 | )(Notifications); 129 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/profile/EditDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | // Redux stuff 6 | import { connect } from 'react-redux'; 7 | import { editUserDetails } from '../../redux/actions/userActions'; 8 | // MUI Stuff 9 | import Button from '@material-ui/core/Button'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Dialog from '@material-ui/core/Dialog'; 12 | import DialogActions from '@material-ui/core/DialogActions'; 13 | import DialogContent from '@material-ui/core/DialogContent'; 14 | import DialogTitle from '@material-ui/core/DialogTitle'; 15 | // Icons 16 | import EditIcon from '@material-ui/icons/Edit'; 17 | 18 | const styles = (theme) => ({ 19 | ...theme, 20 | button: { 21 | float: 'right' 22 | } 23 | }); 24 | 25 | class EditDetails extends Component { 26 | state = { 27 | bio: '', 28 | website: '', 29 | location: '', 30 | open: false 31 | }; 32 | mapUserDetailsToState = (credentials) => { 33 | this.setState({ 34 | bio: credentials.bio ? credentials.bio : '', 35 | website: credentials.website ? credentials.website : '', 36 | location: credentials.location ? credentials.location : '' 37 | }); 38 | }; 39 | handleOpen = () => { 40 | this.setState({ open: true }); 41 | this.mapUserDetailsToState(this.props.credentials); 42 | }; 43 | handleClose = () => { 44 | this.setState({ open: false }); 45 | }; 46 | componentDidMount() { 47 | const { credentials } = this.props; 48 | this.mapUserDetailsToState(credentials); 49 | } 50 | 51 | handleChange = (event) => { 52 | this.setState({ 53 | [event.target.name]: event.target.value 54 | }); 55 | }; 56 | handleSubmit = () => { 57 | const userDetails = { 58 | bio: this.state.bio, 59 | website: this.state.website, 60 | location: this.state.location 61 | }; 62 | this.props.editUserDetails(userDetails); 63 | this.handleClose(); 64 | }; 65 | render() { 66 | const { classes } = this.props; 67 | return ( 68 | 69 | 74 | 75 | 76 | 82 | Edit your details 83 | 84 |
85 | 97 | 107 | 117 | 118 |
119 | 120 | 123 | 126 | 127 |
128 |
129 | ); 130 | } 131 | } 132 | 133 | EditDetails.propTypes = { 134 | editUserDetails: PropTypes.func.isRequired, 135 | classes: PropTypes.object.isRequired 136 | }; 137 | 138 | const mapStateToProps = (state) => ({ 139 | credentials: state.user.credentials 140 | }); 141 | 142 | export default connect( 143 | mapStateToProps, 144 | { editUserDetails } 145 | )(withStyles(styles)(EditDetails)); 146 | -------------------------------------------------------------------------------- /socialmedia-client/src/pages/signup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import withStyles from '@material-ui/core/styles/withStyles'; 3 | import PropTypes from 'prop-types'; 4 | import AppIcon from '../images/icon.png'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | // MUI Stuff 8 | import Grid from '@material-ui/core/Grid'; 9 | import Typography from '@material-ui/core/Typography'; 10 | import TextField from '@material-ui/core/TextField'; 11 | import Button from '@material-ui/core/Button'; 12 | import CircularProgress from '@material-ui/core/CircularProgress'; 13 | // Redux stuff 14 | import { connect } from 'react-redux'; 15 | import { signupUser } from '../redux/actions/userActions'; 16 | 17 | const styles = (theme) => ({ 18 | ...theme 19 | }); 20 | 21 | class signup extends Component { 22 | constructor() { 23 | super(); 24 | this.state = { 25 | email: '', 26 | password: '', 27 | confirmPassword: '', 28 | handle: '', 29 | errors: {} 30 | }; 31 | } 32 | componentWillReceiveProps(nextProps) { 33 | if (nextProps.UI.errors) { 34 | this.setState({ errors: nextProps.UI.errors }); 35 | } 36 | } 37 | handleSubmit = (event) => { 38 | event.preventDefault(); 39 | this.setState({ 40 | loading: true 41 | }); 42 | const newUserData = { 43 | email: this.state.email, 44 | password: this.state.password, 45 | confirmPassword: this.state.confirmPassword, 46 | handle: this.state.handle 47 | }; 48 | this.props.signupUser(newUserData, this.props.history); 49 | }; 50 | handleChange = (event) => { 51 | this.setState({ 52 | [event.target.name]: event.target.value 53 | }); 54 | }; 55 | render() { 56 | const { 57 | classes, 58 | UI: { loading } 59 | } = this.props; 60 | const { errors } = this.state; 61 | 62 | return ( 63 | 64 | 65 | 66 | app icon 67 | 68 | Sign Up 69 | 70 |
71 | 83 | 95 | 107 | 119 | {errors.general && ( 120 | 121 | {errors.general} 122 | 123 | )} 124 | 136 |
137 | 138 | Already have an account ? Login here 139 | 140 | 141 |
142 | 143 | 144 | ); 145 | } 146 | } 147 | 148 | signup.propTypes = { 149 | classes: PropTypes.object.isRequired, 150 | user: PropTypes.object.isRequired, 151 | UI: PropTypes.object.isRequired, 152 | signupUser: PropTypes.func.isRequired 153 | }; 154 | 155 | const mapStateToProps = (state) => ({ 156 | user: state.user, 157 | UI: state.UI 158 | }); 159 | 160 | export default connect( 161 | mapStateToProps, 162 | { signupUser } 163 | )(withStyles(styles)(signup)); 164 | -------------------------------------------------------------------------------- /socialmedia-server/functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions'); 2 | const app = require('express')(); 3 | const FBAuth = require('./util/fbAuth'); 4 | 5 | const cors = require('cors'); 6 | app.use(cors()); 7 | 8 | const { db } = require('./util/admin'); 9 | 10 | const { 11 | getAllScreams, 12 | postOneScream, 13 | getScream, 14 | commentOnScream, 15 | likeScream, 16 | unlikeScream, 17 | deleteScream 18 | } = require('./handlers/screams'); 19 | const { 20 | signup, 21 | login, 22 | uploadImage, 23 | addUserDetails, 24 | getAuthenticatedUser, 25 | getUserDetails, 26 | markNotificationsRead 27 | } = require('./handlers/users'); 28 | 29 | // Scream routes 30 | app.get('/screams', getAllScreams); 31 | app.post('/scream', FBAuth, postOneScream); 32 | app.get('/scream/:screamId', getScream); 33 | app.delete('/scream/:screamId', FBAuth, deleteScream); 34 | app.get('/scream/:screamId/like', FBAuth, likeScream); 35 | app.get('/scream/:screamId/unlike', FBAuth, unlikeScream); 36 | app.post('/scream/:screamId/comment', FBAuth, commentOnScream); 37 | 38 | // users routes 39 | app.post('/signup', signup); 40 | app.post('/login', login); 41 | app.post('/user/image', FBAuth, uploadImage); 42 | app.post('/user', FBAuth, addUserDetails); 43 | app.get('/user', FBAuth, getAuthenticatedUser); 44 | app.get('/user/:handle', getUserDetails); 45 | app.post('/notifications', FBAuth, markNotificationsRead); 46 | 47 | exports.api = functions.https.onRequest(app); 48 | 49 | exports.createNotificationOnLike = functions 50 | .firestore.document('likes/{id}') 51 | .onCreate((snapshot) => { 52 | return db 53 | .doc(`/screams/${snapshot.data().screamId}`) 54 | .get() 55 | .then((doc) => { 56 | if ( 57 | doc.exists && 58 | doc.data().userHandle !== snapshot.data().userHandle 59 | ) { 60 | return db.doc(`/notifications/${snapshot.id}`).set({ 61 | createdAt: new Date().toISOString(), 62 | recipient: doc.data().userHandle, 63 | sender: snapshot.data().userHandle, 64 | type: 'like', 65 | read: false, 66 | screamId: doc.id 67 | }); 68 | } 69 | }) 70 | .catch((err) => console.error(err)); 71 | }); 72 | exports.deleteNotificationOnUnLike = functions 73 | .firestore.document('likes/{id}') 74 | .onDelete((snapshot) => { 75 | return db 76 | .doc(`/notifications/${snapshot.id}`) 77 | .delete() 78 | .catch((err) => { 79 | console.error(err); 80 | return; 81 | }); 82 | }); 83 | exports.createNotificationOnComment = functions 84 | .firestore.document('comments/{id}') 85 | .onCreate((snapshot) => { 86 | return db 87 | .doc(`/screams/${snapshot.data().screamId}`) 88 | .get() 89 | .then((doc) => { 90 | if ( 91 | doc.exists && 92 | doc.data().userHandle !== snapshot.data().userHandle 93 | ) { 94 | return db.doc(`/notifications/${snapshot.id}`).set({ 95 | createdAt: new Date().toISOString(), 96 | recipient: doc.data().userHandle, 97 | sender: snapshot.data().userHandle, 98 | type: 'comment', 99 | read: false, 100 | screamId: doc.id 101 | }); 102 | } 103 | }) 104 | .catch((err) => { 105 | console.error(err); 106 | return; 107 | }); 108 | }); 109 | 110 | exports.onUserImageChange = functions 111 | .firestore.document('/users/{userId}') 112 | .onUpdate((change) => { 113 | console.log(change.before.data()); 114 | console.log(change.after.data()); 115 | if (change.before.data().imageUrl !== change.after.data().imageUrl) { 116 | console.log('image has changed'); 117 | const batch = db.batch(); 118 | return db 119 | .collection('screams') 120 | .where('userHandle', '==', change.before.data().handle) 121 | .get() 122 | .then((data) => { 123 | data.forEach((doc) => { 124 | const scream = db.doc(`/screams/${doc.id}`); 125 | batch.update(scream, { userImage: change.after.data().imageUrl }); 126 | }); 127 | return batch.commit(); 128 | }); 129 | } else return true; 130 | }); 131 | 132 | exports.onScreamDelete = functions 133 | .firestore.document('/screams/{screamId}') 134 | .onDelete((snapshot, context) => { 135 | const screamId = context.params.screamId; 136 | const batch = db.batch(); 137 | return db 138 | .collection('comments') 139 | .where('screamId', '==', screamId) 140 | .get() 141 | .then((data) => { 142 | data.forEach((doc) => { 143 | batch.delete(db.doc(`/comments/${doc.id}`)); 144 | }); 145 | return db 146 | .collection('likes') 147 | .where('screamId', '==', screamId) 148 | .get(); 149 | }) 150 | .then((data) => { 151 | data.forEach((doc) => { 152 | batch.delete(db.doc(`/likes/${doc.id}`)); 153 | }); 154 | return db 155 | .collection('notifications') 156 | .where('screamId', '==', screamId) 157 | .get(); 158 | }) 159 | .then((data) => { 160 | data.forEach((doc) => { 161 | batch.delete(db.doc(`/notifications/${doc.id}`)); 162 | }); 163 | return batch.commit(); 164 | }) 165 | .catch((err) => console.error(err)); 166 | }); 167 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/profile/Profile.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import { Link } from 'react-router-dom'; 5 | import dayjs from 'dayjs'; 6 | import EditDetails from './EditDetails'; 7 | import MyButton from '../../util/MyButton'; 8 | import ProfileSkeleton from '../../util/ProfileSkeleton'; 9 | // MUI stuff 10 | import Button from '@material-ui/core/Button'; 11 | import Typography from '@material-ui/core/Typography'; 12 | import MuiLink from '@material-ui/core/Link'; 13 | import Paper from '@material-ui/core/Paper'; 14 | // Icons 15 | import LocationOn from '@material-ui/icons/LocationOn'; 16 | import LinkIcon from '@material-ui/icons/Link'; 17 | import CalendarToday from '@material-ui/icons/CalendarToday'; 18 | import EditIcon from '@material-ui/icons/Edit'; 19 | import KeyboardReturn from '@material-ui/icons/KeyboardReturn'; 20 | //Redux 21 | import { connect } from 'react-redux'; 22 | import { logoutUser, uploadImage } from '../../redux/actions/userActions'; 23 | 24 | const styles = (theme) => ({ 25 | ...theme 26 | }); 27 | 28 | class Profile extends Component { 29 | handleImageChange = (event) => { 30 | const image = event.target.files[0]; 31 | const formData = new FormData(); 32 | formData.append('image', image, image.name); 33 | this.props.uploadImage(formData); 34 | }; 35 | handleEditPicture = () => { 36 | const fileInput = document.getElementById('imageInput'); 37 | fileInput.click(); 38 | }; 39 | handleLogout = () => { 40 | this.props.logoutUser(); 41 | }; 42 | render() { 43 | const { 44 | classes, 45 | user: { 46 | credentials: { handle, createdAt, imageUrl, bio, website, location }, 47 | loading, 48 | authenticated 49 | } 50 | } = this.props; 51 | 52 | let profileMarkup = !loading ? ( 53 | authenticated ? ( 54 | 55 |
56 |
57 | profile 58 | 64 | 69 | 70 | 71 |
72 |
73 |
74 | 80 | @{handle} 81 | 82 |
83 | {bio && {bio}} 84 |
85 | {location && ( 86 | 87 | {location} 88 |
89 |
90 | )} 91 | {website && ( 92 | 93 | 94 | 95 | {' '} 96 | {website} 97 | 98 |
99 |
100 | )} 101 | {' '} 102 | Joined {dayjs(createdAt).format('MMM YYYY')} 103 |
104 | 105 | 106 | 107 | 108 |
109 |
110 | ) : ( 111 | 112 | 113 | No profile found, please login again 114 | 115 |
116 | 124 | 132 |
133 |
134 | ) 135 | ) : ( 136 | 137 | ); 138 | 139 | return profileMarkup; 140 | } 141 | } 142 | 143 | const mapStateToProps = (state) => ({ 144 | user: state.user 145 | }); 146 | 147 | const mapActionsToProps = { logoutUser, uploadImage }; 148 | 149 | Profile.propTypes = { 150 | logoutUser: PropTypes.func.isRequired, 151 | uploadImage: PropTypes.func.isRequired, 152 | user: PropTypes.object.isRequired, 153 | classes: PropTypes.object.isRequired 154 | }; 155 | 156 | export default connect( 157 | mapStateToProps, 158 | mapActionsToProps 159 | )(withStyles(styles)(Profile)); 160 | -------------------------------------------------------------------------------- /socialmedia-client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /socialmedia-client/src/components/scream/ScreamDialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import withStyles from '@material-ui/core/styles/withStyles'; 4 | import MyButton from '../../util/MyButton'; 5 | import LikeButton from './LikeButton'; 6 | import Comments from './Comments'; 7 | import CommentForm from './CommentForm'; 8 | import dayjs from 'dayjs'; 9 | import { Link } from 'react-router-dom'; 10 | // MUI Stuff 11 | import Dialog from '@material-ui/core/Dialog'; 12 | import DialogContent from '@material-ui/core/DialogContent'; 13 | import CircularProgress from '@material-ui/core/CircularProgress'; 14 | import Grid from '@material-ui/core/Grid'; 15 | import Typography from '@material-ui/core/Typography'; 16 | // Icons 17 | import CloseIcon from '@material-ui/icons/Close'; 18 | import UnfoldMore from '@material-ui/icons/UnfoldMore'; 19 | import ChatIcon from '@material-ui/icons/Chat'; 20 | // Redux stuff 21 | import { connect } from 'react-redux'; 22 | import { getScream, clearErrors } from '../../redux/actions/dataActions'; 23 | 24 | const styles = (theme) => ({ 25 | ...theme, 26 | profileImage: { 27 | maxWidth: 200, 28 | height: 200, 29 | borderRadius: '50%', 30 | objectFit: 'cover' 31 | }, 32 | dialogContent: { 33 | padding: 20 34 | }, 35 | closeButton: { 36 | position: 'absolute', 37 | left: '90%' 38 | }, 39 | expandButton: { 40 | position: 'absolute', 41 | left: '90%' 42 | }, 43 | spinnerDiv: { 44 | textAlign: 'center', 45 | marginTop: 50, 46 | marginBottom: 50 47 | } 48 | }); 49 | 50 | class ScreamDialog extends Component { 51 | state = { 52 | open: false, 53 | oldPath: '', 54 | newPath: '' 55 | }; 56 | componentDidMount() { 57 | if (this.props.openDialog) { 58 | this.handleOpen(); 59 | } 60 | } 61 | handleOpen = () => { 62 | let oldPath = window.location.pathname; 63 | 64 | const { userHandle, screamId } = this.props; 65 | const newPath = `/users/${userHandle}/scream/${screamId}`; 66 | 67 | if (oldPath === newPath) oldPath = `/users/${userHandle}`; 68 | 69 | window.history.pushState(null, null, newPath); 70 | 71 | this.setState({ open: true, oldPath, newPath }); 72 | this.props.getScream(this.props.screamId); 73 | }; 74 | handleClose = () => { 75 | window.history.pushState(null, null, this.state.oldPath); 76 | this.setState({ open: false }); 77 | this.props.clearErrors(); 78 | window.location.reload(); 79 | }; 80 | 81 | render() { 82 | const { 83 | classes, 84 | scream: { 85 | screamId, 86 | body, 87 | createdAt, 88 | likeCount, 89 | commentCount, 90 | userImage, 91 | userHandle, 92 | comments 93 | }, 94 | UI: { loading } 95 | } = this.props; 96 | 97 | const dialogMarkup = loading ? ( 98 |
99 | 100 |
101 | ) : ( 102 | 103 | 104 | Profile 105 | 106 | 107 | 113 | @{userHandle} 114 | 115 |
116 | 117 | {dayjs(createdAt).format('h:mm a, MMMM DD YYYY')} 118 | 119 |
120 | {body} 121 | 122 | {likeCount} Likes 123 | 124 | 125 | 126 | {commentCount} Comments 127 |
128 |
129 | 130 | 131 |
132 | ); 133 | return ( 134 | 135 | 140 | 141 | 142 | 148 | 153 | 154 | 155 | 156 | {dialogMarkup} 157 | 158 | 159 | 160 | ); 161 | } 162 | } 163 | 164 | ScreamDialog.propTypes = { 165 | clearErrors: PropTypes.func.isRequired, 166 | getScream: PropTypes.func.isRequired, 167 | screamId: PropTypes.string.isRequired, 168 | userHandle: PropTypes.string.isRequired, 169 | scream: PropTypes.object.isRequired, 170 | UI: PropTypes.object.isRequired 171 | }; 172 | 173 | const mapStateToProps = (state) => ({ 174 | scream: state.data.scream, 175 | UI: state.UI 176 | }); 177 | 178 | const mapActionsToProps = { 179 | getScream, 180 | clearErrors 181 | }; 182 | 183 | export default connect( 184 | mapStateToProps, 185 | mapActionsToProps 186 | )(withStyles(styles)(ScreamDialog)); 187 | -------------------------------------------------------------------------------- /socialmedia-server/functions/handlers/screams.js: -------------------------------------------------------------------------------- 1 | const { db } = require('../util/admin'); 2 | 3 | exports.getAllScreams = (req, res) => { 4 | db.collection('screams') 5 | .orderBy('createdAt', 'desc') 6 | .get() 7 | .then((data) => { 8 | let screams = []; 9 | data.forEach((doc) => { 10 | screams.push({ 11 | screamId: doc.id, 12 | body: doc.data().body, 13 | userHandle: doc.data().userHandle, 14 | createdAt: doc.data().createdAt, 15 | commentCount: doc.data().commentCount, 16 | likeCount: doc.data().likeCount, 17 | userImage: doc.data().userImage 18 | }); 19 | }); 20 | return res.json(screams); 21 | }) 22 | .catch((err) => { 23 | console.error(err); 24 | res.status(500).json({ error: err.code }); 25 | }); 26 | }; 27 | 28 | exports.postOneScream = (req, res) => { 29 | if (typeof req.body.body !== 'string' || req.body.body.trim() === '') { 30 | return res.status(400).json({ body: 'Body must not be empty' }); 31 | } 32 | 33 | const newScream = { 34 | body: req.body.body, 35 | userHandle: req.user.handle, 36 | userImage: req.user.imageUrl, 37 | createdAt: new Date().toISOString(), 38 | likeCount: 0, 39 | commentCount: 0 40 | }; 41 | 42 | db.collection('screams') 43 | .add(newScream) 44 | .then((doc) => { 45 | const resScream = newScream; 46 | resScream.screamId = doc.id; 47 | res.json(resScream); 48 | }) 49 | .catch((err) => { 50 | res.status(500).json({ error: 'something went wrong' }); 51 | console.error(err); 52 | }); 53 | }; 54 | // Fetch one scream 55 | exports.getScream = (req, res) => { 56 | let screamData = {}; 57 | db.doc(`/screams/${req.params.screamId}`) 58 | .get() 59 | .then((doc) => { 60 | if (!doc.exists) { 61 | return res.status(404).json({ error: 'Scream not found' }); 62 | } 63 | screamData = doc.data(); 64 | screamData.screamId = doc.id; 65 | return db 66 | .collection('comments') 67 | .orderBy('createdAt', 'desc') 68 | .where('screamId', '==', req.params.screamId) 69 | .get(); 70 | }) 71 | .then((data) => { 72 | screamData.comments = []; 73 | data.forEach((doc) => { 74 | screamData.comments.push(doc.data()); 75 | }); 76 | return res.json(screamData); 77 | }) 78 | .catch((err) => { 79 | console.error(err); 80 | res.status(500).json({ error: err.code }); 81 | }); 82 | }; 83 | // Comment on a comment 84 | exports.commentOnScream = (req, res) => { 85 | if (typeof req.body.body !== 'string' || req.body.body.trim() === '') 86 | return res.status(400).json({ comment: 'Must not be empty' }); 87 | 88 | const newComment = { 89 | body: req.body.body, 90 | createdAt: new Date().toISOString(), 91 | screamId: req.params.screamId, 92 | userHandle: req.user.handle, 93 | userImage: req.user.imageUrl 94 | }; 95 | console.log(newComment); 96 | 97 | db.doc(`/screams/${req.params.screamId}`) 98 | .get() 99 | .then((doc) => { 100 | if (!doc.exists) { 101 | return res.status(404).json({ error: 'Scream not found' }); 102 | } 103 | return doc.ref.update({ commentCount: doc.data().commentCount + 1 }); 104 | }) 105 | .then(() => { 106 | return db.collection('comments').add(newComment); 107 | }) 108 | .then(() => { 109 | res.json(newComment); 110 | }) 111 | .catch((err) => { 112 | console.log(err); 113 | res.status(500).json({ error: 'Something went wrong' }); 114 | }); 115 | }; 116 | // Like a scream 117 | exports.likeScream = (req, res) => { 118 | const likeDocument = db 119 | .collection('likes') 120 | .where('userHandle', '==', req.user.handle) 121 | .where('screamId', '==', req.params.screamId) 122 | .limit(1); 123 | 124 | const screamDocument = db.doc(`/screams/${req.params.screamId}`); 125 | 126 | let screamData; 127 | 128 | screamDocument 129 | .get() 130 | .then((doc) => { 131 | if (doc.exists) { 132 | screamData = doc.data(); 133 | screamData.screamId = doc.id; 134 | return likeDocument.get(); 135 | } else { 136 | return res.status(404).json({ error: 'Scream not found' }); 137 | } 138 | }) 139 | .then((data) => { 140 | if (data.empty) { 141 | return db 142 | .collection('likes') 143 | .add({ 144 | screamId: req.params.screamId, 145 | userHandle: req.user.handle 146 | }) 147 | .then(() => { 148 | screamData.likeCount++; 149 | return screamDocument.update({ likeCount: screamData.likeCount }); 150 | }) 151 | .then(() => { 152 | return res.json(screamData); 153 | }); 154 | } else { 155 | return res.status(400).json({ error: 'Scream already liked' }); 156 | } 157 | }) 158 | .catch((err) => { 159 | console.error(err); 160 | res.status(500).json({ error: err.code }); 161 | }); 162 | }; 163 | 164 | exports.unlikeScream = (req, res) => { 165 | const likeDocument = db 166 | .collection('likes') 167 | .where('userHandle', '==', req.user.handle) 168 | .where('screamId', '==', req.params.screamId) 169 | .limit(1); 170 | 171 | const screamDocument = db.doc(`/screams/${req.params.screamId}`); 172 | 173 | let screamData; 174 | 175 | screamDocument 176 | .get() 177 | .then((doc) => { 178 | if (doc.exists) { 179 | screamData = doc.data(); 180 | screamData.screamId = doc.id; 181 | return likeDocument.get(); 182 | } else { 183 | return res.status(404).json({ error: 'Scream not found' }); 184 | } 185 | }) 186 | .then((data) => { 187 | if (data.empty) { 188 | return res.status(400).json({ error: 'Scream not liked' }); 189 | } else { 190 | return db 191 | .doc(`/likes/${data.docs[0].id}`) 192 | .delete() 193 | .then(() => { 194 | screamData.likeCount--; 195 | return screamDocument.update({ likeCount: screamData.likeCount }); 196 | }) 197 | .then(() => { 198 | res.json(screamData); 199 | }); 200 | } 201 | }) 202 | .catch((err) => { 203 | console.error(err); 204 | res.status(500).json({ error: err.code }); 205 | }); 206 | }; 207 | // Delete a scream 208 | exports.deleteScream = (req, res) => { 209 | const document = db.doc(`/screams/${req.params.screamId}`); 210 | document 211 | .get() 212 | .then((doc) => { 213 | if (!doc.exists) { 214 | return res.status(404).json({ error: 'Scream not found' }); 215 | } 216 | if (doc.data().userHandle !== req.user.handle) { 217 | return res.status(403).json({ error: 'Unauthorized' }); 218 | } else { 219 | return document.delete(); 220 | } 221 | }) 222 | .then(() => { 223 | res.json({ message: 'Scream deleted successfully' }); 224 | }) 225 | .catch((err) => { 226 | console.error(err); 227 | return res.status(500).json({ error: err.code }); 228 | }); 229 | }; 230 | -------------------------------------------------------------------------------- /socialmedia-server/firebase-debug.log: -------------------------------------------------------------------------------- 1 | [debug] [2019-08-17T18:12:56.331Z] ---------------------------------------------------------------------- 2 | [debug] [2019-08-17T18:12:56.335Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Daniel-Laptop\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js serve --only functions --debug 3 | [debug] [2019-08-17T18:12:56.336Z] CLI Version: 7.2.2 4 | [debug] [2019-08-17T18:12:56.336Z] Platform: win32 5 | [debug] [2019-08-17T18:12:56.337Z] Node Version: v10.16.2 6 | [debug] [2019-08-17T18:12:56.338Z] Time: Sat Aug 17 2019 14:12:56 GMT-0400 (Eastern Daylight Time) 7 | [debug] [2019-08-17T18:12:56.338Z] ---------------------------------------------------------------------- 8 | [debug] 9 | [debug] [2019-08-17T18:12:56.347Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 10 | [debug] [2019-08-17T18:12:56.348Z] > authorizing via signed-in user 11 | [debug] [2019-08-17T18:12:56.349Z] [iam] checking project socialape-7d0b6 for permissions ["firebase.projects.get"] 12 | [debug] [2019-08-17T18:12:56.351Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/socialape-7d0b6:testIamPermissions 13 | 14 | [debug] [2019-08-17T18:12:56.592Z] <<< HTTP RESPONSE 200 15 | [debug] [2019-08-17T18:12:56.596Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6 16 | 17 | [debug] [2019-08-17T18:12:56.789Z] <<< HTTP RESPONSE 200 18 | [warn] ! Your requested "node" version "8" doesn't match your global version "10" 19 | [debug] [2019-08-17T18:12:56.806Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6/adminSdkConfig 20 | 21 | [debug] [2019-08-17T18:12:56.945Z] <<< HTTP RESPONSE 200 22 | [info] + functions: Emulator started at http://localhost:5000 23 | [info] i functions: Watching "C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions" for Cloud Functions... 24 | [debug] [2019-08-17T18:12:57.664Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 25 | [debug] [2019-08-17T18:12:57.666Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 26 | [debug] [2019-08-17T18:12:57.669Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 27 | [debug] [2019-08-17T18:12:57.671Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 28 | [debug] [2019-08-17T18:12:57.673Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 29 | [info] + functions[api]: http function initialized (http://localhost:5000/socialape-7d0b6/us-central1/api). 30 | [info] i functions[createNotificationOnLike]: function ignored because the firestore emulator does not exist or is not running. 31 | [info] i functions[deleteNotificationOnUnLike]: function ignored because the firestore emulator does not exist or is not running. 32 | [info] i functions[createNotificationOnComment]: function ignored because the firestore emulator does not exist or is not running. 33 | [info] i functions[onUserImageChange]: function ignored because the firestore emulator does not exist or is not running. 34 | [info] i functions[onScreamDelete]: function ignored because the firestore emulator does not exist or is not running. 35 | [info] Shutting down... 36 | [debug] [2019-08-17T18:17:35.209Z] ---------------------------------------------------------------------- 37 | [debug] [2019-08-17T18:17:35.213Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Daniel-Laptop\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js serve --only functions --debug 38 | [debug] [2019-08-17T18:17:35.214Z] CLI Version: 7.2.2 39 | [debug] [2019-08-17T18:17:35.215Z] Platform: win32 40 | [debug] [2019-08-17T18:17:35.215Z] Node Version: v10.16.2 41 | [debug] [2019-08-17T18:17:35.216Z] Time: Sat Aug 17 2019 14:17:35 GMT-0400 (Eastern Daylight Time) 42 | [debug] [2019-08-17T18:17:35.217Z] ---------------------------------------------------------------------- 43 | [debug] 44 | [debug] [2019-08-17T18:17:35.227Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 45 | [debug] [2019-08-17T18:17:35.228Z] > authorizing via signed-in user 46 | [debug] [2019-08-17T18:17:35.229Z] [iam] checking project socialape-7d0b6 for permissions ["firebase.projects.get"] 47 | [debug] [2019-08-17T18:17:35.231Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/socialape-7d0b6:testIamPermissions 48 | 49 | [debug] [2019-08-17T18:17:35.937Z] <<< HTTP RESPONSE 200 50 | [debug] [2019-08-17T18:17:35.941Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6 51 | 52 | [debug] [2019-08-17T18:17:36.207Z] <<< HTTP RESPONSE 200 53 | [warn] ! Your requested "node" version "8" doesn't match your global version "10" 54 | [debug] [2019-08-17T18:17:36.226Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6/adminSdkConfig 55 | 56 | [debug] [2019-08-17T18:17:36.384Z] <<< HTTP RESPONSE 200 57 | [info] + functions: Emulator started at http://localhost:5000 58 | [info] i functions: Watching "C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions" for Cloud Functions... 59 | [debug] [2019-08-17T18:17:37.094Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 60 | [debug] [2019-08-17T18:17:37.095Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 61 | [debug] [2019-08-17T18:17:37.098Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 62 | [debug] [2019-08-17T18:17:37.100Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 63 | [debug] [2019-08-17T18:17:37.101Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 64 | [info] + functions[api]: http function initialized (http://localhost:5000/socialape-7d0b6/us-central1/api). 65 | [info] i functions[createNotificationOnLike]: function ignored because the firestore emulator does not exist or is not running. 66 | [info] i functions[deleteNotificationOnUnLike]: function ignored because the firestore emulator does not exist or is not running. 67 | [info] i functions[createNotificationOnComment]: function ignored because the firestore emulator does not exist or is not running. 68 | [info] i functions[onUserImageChange]: function ignored because the firestore emulator does not exist or is not running. 69 | [info] i functions[onScreamDelete]: function ignored because the firestore emulator does not exist or is not running. 70 | [info] Shutting down... 71 | -------------------------------------------------------------------------------- /socialmedia-server/functions/handlers/users.js: -------------------------------------------------------------------------------- 1 | // Entire file content, but only vulnerable parts should be modified minimally 2 | const { admin, db } = require('../util/admin'); 3 | 4 | const config = require('../util/config'); 5 | 6 | const firebase = require('firebase'); 7 | firebase.initializeApp(config); 8 | 9 | const { 10 | validateSignupData, 11 | validateLoginData, 12 | reduceUserDetails 13 | } = require('../util/validators'); 14 | 15 | // Sign users up 16 | exports.signup = (req, res) => { 17 | const newUser = { 18 | email: req.body.email, 19 | password: req.body.password, 20 | confirmPassword: req.body.confirmPassword, 21 | handle: req.body.handle 22 | }; 23 | 24 | const { valid, errors } = validateSignupData(newUser); 25 | 26 | if (!valid) return res.status(400).json(errors); 27 | 28 | const noImg = 'no-img.png'; 29 | 30 | let token, userId; 31 | db.doc(`/users/${newUser.handle}`) 32 | .get() 33 | .then((doc) => { 34 | if (doc.exists) { 35 | return res.status(400).json({ handle: 'this handle is already taken' }); 36 | } else { 37 | return firebase 38 | .auth() 39 | .createUserWithEmailAndPassword(newUser.email, newUser.password); 40 | } 41 | }) 42 | .then((data) => { 43 | userId = data.user.uid; 44 | return data.user.getIdToken(); 45 | }) 46 | .then((idToken) => { 47 | token = idToken; 48 | const userCredentials = { 49 | handle: newUser.handle, 50 | email: newUser.email, 51 | createdAt: new Date().toISOString(), 52 | imageUrl: `https://firebasestorage.googleapis.com/v0/b/${ 53 | config.storageBucket 54 | }/o/${noImg}?alt=media`, 55 | userId 56 | }; 57 | return db.doc(`/users/${newUser.handle}`).set(userCredentials); 58 | }) 59 | .then(() => { 60 | return res.status(201).json({ token }); 61 | }) 62 | .catch((err) => { 63 | console.error(err); 64 | if (err.code === 'auth/email-already-in-use') { 65 | return res.status(400).json({ email: 'Email is already is use' }); 66 | } else { 67 | return res 68 | .status(500) 69 | .json({ general: err.message }); 70 | } 71 | }); 72 | }; 73 | // Log user in 74 | exports.login = (req, res) => { 75 | const user = { 76 | email: req.body.email, 77 | password: req.body.password 78 | }; 79 | 80 | const { valid, errors } = validateLoginData(user); 81 | 82 | if (!valid) return res.status(400).json(errors); 83 | 84 | firebase 85 | .auth() 86 | .signInWithEmailAndPassword(user.email, user.password) 87 | .then((data) => { 88 | return data.user.getIdToken(); 89 | }) 90 | .then((token) => { 91 | return res.json({ token }); 92 | }) 93 | .catch((err) => { 94 | console.error(err); 95 | // auth/wrong-password 96 | // auth/user-not-user 97 | return res 98 | .status(403) 99 | .json({ general: 'Wrong credentials, please try again' }); 100 | }); 101 | }; 102 | 103 | // Add user details 104 | exports.addUserDetails = (req, res) => { 105 | let userDetails = reduceUserDetails(req.body); 106 | 107 | db.doc(`/users/${req.user.handle}`) 108 | .update(userDetails) 109 | .then(() => { 110 | return res.json({ message: 'Details added successfully' }); 111 | }) 112 | .catch((err) => { 113 | console.error(err); 114 | return res.status(500).json({ error: err.code }); 115 | }); 116 | }; 117 | // Get any user's details 118 | exports.getUserDetails = (req, res) => { 119 | let userData = {}; 120 | db.doc(`/users/${req.params.handle}`) 121 | .get() 122 | .then((doc) => { 123 | if (doc.exists) { 124 | userData.user = doc.data(); 125 | return db 126 | .collection('screams') 127 | .where('userHandle', '==', req.params.handle) 128 | .orderBy('createdAt', 'desc') 129 | .get(); 130 | } else { 131 | return res.status(404).json({ errror: 'User not found' }); 132 | } 133 | }) 134 | .then((data) => { 135 | userData.screams = []; 136 | data.forEach((doc) => { 137 | userData.screams.push({ 138 | body: doc.data().body, 139 | createdAt: doc.data().createdAt, 140 | userHandle: doc.data().userHandle, 141 | userImage: doc.data().userImage, 142 | likeCount: doc.data().likeCount, 143 | commentCount: doc.data().commentCount, 144 | screamId: doc.id 145 | }); 146 | }); 147 | return res.json(userData); 148 | }) 149 | .catch((err) => { 150 | console.error(err); 151 | return res.status(500).json({ error: err.code }); 152 | }); 153 | }; 154 | // Get own user details 155 | exports.getAuthenticatedUser = (req, res) => { 156 | let userData = {}; 157 | db.doc(`/users/${req.user.handle}`) 158 | .get() 159 | .then((doc) => { 160 | if (doc.exists) { 161 | userData.credentials = doc.data(); 162 | return db 163 | .collection('likes') 164 | .where('userHandle', '==', req.user.handle) 165 | .get(); 166 | } 167 | }) 168 | .then((data) => { 169 | userData.likes = []; 170 | data.forEach((doc) => { 171 | userData.likes.push(doc.data()); 172 | }); 173 | return db 174 | .collection('notifications') 175 | .where('recipient', '==', req.user.handle) 176 | .orderBy('createdAt', 'desc') 177 | .limit(10) 178 | .get(); 179 | }) 180 | .then((data) => { 181 | userData.notifications = []; 182 | data.forEach((doc) => { 183 | userData.notifications.push({ 184 | recipient: doc.data().recipient, 185 | sender: doc.data().sender, 186 | createdAt: doc.data().createdAt, 187 | screamId: doc.data().screamId, 188 | type: doc.data().type, 189 | read: doc.data().read, 190 | notificationId: doc.id 191 | }); 192 | }); 193 | return res.json(userData); 194 | }) 195 | .catch((err) => { 196 | console.error(err); 197 | return res.status(500).json({ error: err.code }); 198 | }); 199 | }; 200 | // Upload a profile image for user 201 | exports.uploadImage = (req, res) => { 202 | const BusBoy = require('busboy'); 203 | const path = require('path'); 204 | const os = require('os'); 205 | const fs = require('fs'); 206 | 207 | const busboy = new BusBoy({ headers: req.headers }); 208 | 209 | let imageToBeUploaded = {}; 210 | let imageFileName; 211 | 212 | busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { 213 | console.log(fieldname, file, filename, encoding, mimetype); 214 | if (mimetype !== 'image/jpeg' && mimetype !== 'image/png') { 215 | return res.status(400).json({ error: 'Wrong file type submitted' }); 216 | } 217 | // my.image.png => ['my', 'image', 'png'] 218 | const imageExtension = filename.split('.')[filename.split('.').length - 1]; 219 | // 32756238461724837.png 220 | imageFileName = `${Math.round( 221 | Math.random() * 1000000000000 222 | ).toString()}.${imageExtension}`; 223 | const filepath = path.join(os.tmpdir(), path.basename(imageFileName)); // Sanitize the file path 224 | imageToBeUploaded = { filepath, mimetype }; 225 | file.pipe(fs.createWriteStream(filepath)); 226 | }); 227 | busboy.on('finish', () => { 228 | admin 229 | .storage() 230 | .bucket() 231 | .upload(imageToBeUploaded.filepath, { 232 | resumable: false, 233 | metadata: { 234 | metadata: { 235 | contentType: imageToBeUploaded.mimetype 236 | } 237 | } 238 | }) 239 | .then(() => { 240 | const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${ 241 | config.storageBucket 242 | }/o/${imageFileName}?alt=media`; 243 | return db.doc(`/users/${req.user.handle}`).update({ imageUrl }); 244 | }) 245 | .then(() => { 246 | return res.json({ message: 'image uploaded successfully' }); 247 | }) 248 | .catch((err) => { 249 | console.error(err); 250 | return res.status(500).json({ error: 'something went wrong' }); 251 | }); 252 | }); 253 | busboy.end(req.rawBody); 254 | }; 255 | 256 | exports.markNotificationsRead = (req, res) => { 257 | if (!Array.isArray(req.body)) { 258 | return res.status(400).json({ error: 'Invalid input' }); 259 | } 260 | let batch = db.batch(); 261 | req.body.forEach((notificationId) => { 262 | const notification = db.doc(`/notifications/${notificationId}`); 263 | batch.update(notification, { read: true }); 264 | }); 265 | batch 266 | .commit() 267 | .then(() => { 268 | return res.json({ message: 'Notifications marked read' }); 269 | }) 270 | .catch((err) => { 271 | console.error(err); 272 | return res.status(500).json({ error: err.code }); 273 | }); 274 | }; 275 | -------------------------------------------------------------------------------- /socialmedia-server/functions/firebase-debug.log: -------------------------------------------------------------------------------- 1 | [debug] [2019-08-17T18:18:31.422Z] ---------------------------------------------------------------------- 2 | [debug] [2019-08-17T18:18:31.427Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Daniel-Laptop\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js serve --only functions --debug 3 | [debug] [2019-08-17T18:18:31.428Z] CLI Version: 7.2.2 4 | [debug] [2019-08-17T18:18:31.428Z] Platform: win32 5 | [debug] [2019-08-17T18:18:31.429Z] Node Version: v10.16.2 6 | [debug] [2019-08-17T18:18:31.430Z] Time: Sat Aug 17 2019 14:18:31 GMT-0400 (Eastern Daylight Time) 7 | [debug] [2019-08-17T18:18:31.431Z] ---------------------------------------------------------------------- 8 | [debug] 9 | [debug] [2019-08-17T18:18:31.441Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 10 | [debug] [2019-08-17T18:18:31.443Z] > authorizing via signed-in user 11 | [debug] [2019-08-17T18:18:31.443Z] [iam] checking project socialape-7d0b6 for permissions ["firebase.projects.get"] 12 | [debug] [2019-08-17T18:18:31.446Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/socialape-7d0b6:testIamPermissions 13 | 14 | [debug] [2019-08-17T18:18:31.830Z] <<< HTTP RESPONSE 200 15 | [debug] [2019-08-17T18:18:31.835Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6 16 | 17 | [debug] [2019-08-17T18:18:32.101Z] <<< HTTP RESPONSE 200 18 | [warn] ! Your requested "node" version "8" doesn't match your global version "10" 19 | [debug] [2019-08-17T18:18:32.117Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6/adminSdkConfig 20 | 21 | [debug] [2019-08-17T18:18:32.253Z] <<< HTTP RESPONSE 200 22 | [info] + functions: Emulator started at http://localhost:5000 23 | [info] i functions: Watching "C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions" for Cloud Functions... 24 | [debug] [2019-08-17T18:18:32.956Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 25 | [debug] [2019-08-17T18:18:32.957Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 26 | [debug] [2019-08-17T18:18:32.960Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 27 | [debug] [2019-08-17T18:18:32.962Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 28 | [debug] [2019-08-17T18:18:32.963Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 29 | [info] + functions[api]: http function initialized (http://localhost:5000/socialape-7d0b6/us-central1/api). 30 | [info] i functions[createNotificationOnLike]: function ignored because the firestore emulator does not exist or is not running. 31 | [info] i functions[deleteNotificationOnUnLike]: function ignored because the firestore emulator does not exist or is not running. 32 | [info] i functions[createNotificationOnComment]: function ignored because the firestore emulator does not exist or is not running. 33 | [info] i functions[onUserImageChange]: function ignored because the firestore emulator does not exist or is not running. 34 | [info] i functions[onScreamDelete]: function ignored because the firestore emulator does not exist or is not running. 35 | [debug] [2019-08-17T18:24:36.373Z] File C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\util\admin.js changed, reloading triggers 36 | [debug] [2019-08-17T18:24:37.944Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 37 | [debug] [2019-08-17T18:24:37.945Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 38 | [debug] [2019-08-17T18:24:37.949Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 39 | [debug] [2019-08-17T18:24:37.954Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 40 | [debug] [2019-08-17T18:24:37.957Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 41 | [warn] ! Error: The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services. 42 | at FirebaseAppError.FirebaseError [as constructor] (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:42:28) 43 | at FirebaseAppError.PrefixedFirebaseError [as constructor] (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:88:28) 44 | at new FirebaseAppError (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:122:28) 45 | at FirebaseNamespaceInternals.app (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:101:19) 46 | at FirebaseNamespace.app (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:402:30) 47 | at FirebaseNamespace.ensureApp (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:418:24) 48 | at FirebaseNamespace.fn (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:327:30) 49 | at Object. (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\util\admin.js:5:18) 50 | at Module._compile (internal/modules/cjs/loader.js:778:30) 51 | at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10) 52 | [warn] ! We were unable to load your functions code. (see above) 53 | [info] Shutting down... 54 | [debug] [2019-08-17T18:24:52.308Z] ---------------------------------------------------------------------- 55 | [debug] [2019-08-17T18:24:52.312Z] Command: C:\Program Files\nodejs\node.exe C:\Users\Daniel-Laptop\AppData\Roaming\npm\node_modules\firebase-tools\lib\bin\firebase.js serve --only functions --debug 56 | [debug] [2019-08-17T18:24:52.313Z] CLI Version: 7.2.2 57 | [debug] [2019-08-17T18:24:52.314Z] Platform: win32 58 | [debug] [2019-08-17T18:24:52.315Z] Node Version: v10.16.2 59 | [debug] [2019-08-17T18:24:52.316Z] Time: Sat Aug 17 2019 14:24:52 GMT-0400 (Eastern Daylight Time) 60 | [debug] [2019-08-17T18:24:52.317Z] ---------------------------------------------------------------------- 61 | [debug] 62 | [debug] [2019-08-17T18:24:52.325Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"] 63 | [debug] [2019-08-17T18:24:52.327Z] > authorizing via signed-in user 64 | [debug] [2019-08-17T18:24:52.327Z] [iam] checking project socialape-7d0b6 for permissions ["firebase.projects.get"] 65 | [debug] [2019-08-17T18:24:52.329Z] >>> HTTP REQUEST POST https://cloudresourcemanager.googleapis.com/v1/projects/socialape-7d0b6:testIamPermissions 66 | 67 | [debug] [2019-08-17T18:24:52.740Z] <<< HTTP RESPONSE 200 68 | [debug] [2019-08-17T18:24:52.744Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6 69 | 70 | [debug] [2019-08-17T18:24:53.012Z] <<< HTTP RESPONSE 200 71 | [warn] ! Your requested "node" version "8" doesn't match your global version "10" 72 | [debug] [2019-08-17T18:24:53.028Z] >>> HTTP REQUEST GET https://firebase.googleapis.com/v1beta1/projects/socialape-7d0b6/adminSdkConfig 73 | 74 | [debug] [2019-08-17T18:24:53.176Z] <<< HTTP RESPONSE 200 75 | [info] + functions: Emulator started at http://localhost:5000 76 | [info] i functions: Watching "C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions" for Cloud Functions... 77 | [debug] [2019-08-17T18:24:53.872Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 78 | [debug] [2019-08-17T18:24:53.873Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 79 | [debug] [2019-08-17T18:24:53.876Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 80 | [debug] [2019-08-17T18:24:53.878Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 81 | [debug] [2019-08-17T18:24:53.879Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 82 | [warn] ! Error: The default Firebase app does not exist. Make sure you call initializeApp() before using any of the Firebase services. 83 | at FirebaseAppError.FirebaseError [as constructor] (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:42:28) 84 | at FirebaseAppError.PrefixedFirebaseError [as constructor] (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:88:28) 85 | at new FirebaseAppError (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\utils\error.js:122:28) 86 | at FirebaseNamespaceInternals.app (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:101:19) 87 | at FirebaseNamespace.app (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:402:30) 88 | at FirebaseNamespace.ensureApp (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:418:24) 89 | at FirebaseNamespace.fn (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\node_modules\firebase-admin\lib\firebase-namespace.js:327:30) 90 | at Object. (C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\util\admin.js:5:18) 91 | at Module._compile (internal/modules/cjs/loader.js:778:30) 92 | at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10) 93 | [warn] ! We were unable to load your functions code. (see above) 94 | [debug] [2019-08-17T18:25:02.626Z] File C:\Users\Daniel-Laptop\Documents\socialape-functions (new)\functions\util\admin.js changed, reloading triggers 95 | [debug] [2019-08-17T18:25:04.075Z] [runtime-status] Functions runtime initialized. {"cwd":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions","node_version":"10.16.2"} 96 | [debug] [2019-08-17T18:25:04.076Z] [runtime-status] Disabled runtime features: {"functions_config_helper":true,"network_filtering":true,"timeout":true,"memory_limiting":true,"admin_stubs":true} {} 97 | [debug] [2019-08-17T18:25:04.079Z] [runtime-status] Resolved module firebase-admin {"declared":true,"installed":true,"version":"8.3.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-admin\\lib\\index.js"} 98 | [debug] [2019-08-17T18:25:04.082Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 99 | [debug] [2019-08-17T18:25:04.083Z] [runtime-status] Resolved module firebase-functions {"declared":true,"installed":true,"version":"3.2.0","resolution":"C:\\Users\\Daniel-Laptop\\Documents\\socialape-functions (new)\\functions\\node_modules\\firebase-functions\\lib\\index.js"} 100 | [info] + functions[api]: http function initialized (http://localhost:5000/socialape-7d0b6/us-central1/api). 101 | [info] i functions[createNotificationOnLike]: function ignored because the firestore emulator does not exist or is not running. 102 | [info] i functions[deleteNotificationOnUnLike]: function ignored because the firestore emulator does not exist or is not running. 103 | [info] i functions[createNotificationOnComment]: function ignored because the firestore emulator does not exist or is not running. 104 | [info] i functions[onUserImageChange]: function ignored because the firestore emulator does not exist or is not running. 105 | [info] i functions[onScreamDelete]: function ignored because the firestore emulator does not exist or is not running. 106 | -------------------------------------------------------------------------------- /socialmedia-server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@firebase/app": { 6 | "version": "0.4.14", 7 | "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.4.14.tgz", 8 | "integrity": "sha512-2awiJkYz/SMOAEGg5Qvq/596RywoefRckhjUS42kZC7C1HZ06EcQHr26ejAMFk9qeXWYLs4eumSw/V4SDYR3Kw==", 9 | "requires": { 10 | "@firebase/app-types": "0.4.3", 11 | "@firebase/logger": "0.1.22", 12 | "@firebase/util": "0.2.25", 13 | "dom-storage": "2.1.0", 14 | "tslib": "1.10.0", 15 | "xmlhttprequest": "1.8.0" 16 | } 17 | }, 18 | "@firebase/app-types": { 19 | "version": "0.4.3", 20 | "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.4.3.tgz", 21 | "integrity": "sha512-VU5c+ZjejvefLVH4cjiX3Hy1w9HYMv7TtZ1tF9ZmOqT4DSIU1a3VISWoo8///cGGffr5IirMO+Q/WZLI4p8VcA==" 22 | }, 23 | "@firebase/logger": { 24 | "version": "0.1.22", 25 | "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.22.tgz", 26 | "integrity": "sha512-os1vG5FohEF9gl27duZeTtEphOP7oHQ+YjnT+sT2dGprkTIAyaEkzH6G8AgLPUqmASSsoa6BqY5kFXHQi9+xGw==" 27 | }, 28 | "@firebase/util": { 29 | "version": "0.2.25", 30 | "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.25.tgz", 31 | "integrity": "sha512-J/JgYhvFLCpejzfzjzNDZGFZD3kNtTlMu+2EjiQ3tCII6w0N/uEza5GtFiYTKCjGBa51Lmi2j/OPLz+yhlQCWg==", 32 | "requires": { 33 | "tslib": "1.10.0" 34 | } 35 | }, 36 | "@types/body-parser": { 37 | "version": "1.17.0", 38 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", 39 | "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", 40 | "requires": { 41 | "@types/connect": "*", 42 | "@types/node": "*" 43 | } 44 | }, 45 | "@types/connect": { 46 | "version": "3.4.32", 47 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", 48 | "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", 49 | "requires": { 50 | "@types/node": "*" 51 | } 52 | }, 53 | "@types/express": { 54 | "version": "4.17.0", 55 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", 56 | "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", 57 | "requires": { 58 | "@types/body-parser": "*", 59 | "@types/express-serve-static-core": "*", 60 | "@types/serve-static": "*" 61 | } 62 | }, 63 | "@types/express-serve-static-core": { 64 | "version": "4.16.8", 65 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.8.tgz", 66 | "integrity": "sha512-5iLrUAEje8R1Jw6Em7ryETfZbhGc2CAO51Xphnlw7qmGI79f8sG8qMnvMk3M/IxNdoELYalib7ziuD6kUTk7sQ==", 67 | "requires": { 68 | "@types/node": "*", 69 | "@types/range-parser": "*" 70 | } 71 | }, 72 | "@types/mime": { 73 | "version": "2.0.1", 74 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", 75 | "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" 76 | }, 77 | "@types/node": { 78 | "version": "12.7.2", 79 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", 80 | "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==" 81 | }, 82 | "@types/range-parser": { 83 | "version": "1.2.3", 84 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", 85 | "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" 86 | }, 87 | "@types/serve-static": { 88 | "version": "1.13.2", 89 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", 90 | "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", 91 | "requires": { 92 | "@types/express-serve-static-core": "*", 93 | "@types/mime": "*" 94 | } 95 | }, 96 | "accepts": { 97 | "version": "1.3.7", 98 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 99 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 100 | "requires": { 101 | "mime-types": "~2.1.24", 102 | "negotiator": "0.6.2" 103 | } 104 | }, 105 | "array-flatten": { 106 | "version": "1.1.1", 107 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 108 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 109 | }, 110 | "body-parser": { 111 | "version": "1.19.0", 112 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 113 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 114 | "requires": { 115 | "bytes": "3.1.0", 116 | "content-type": "~1.0.4", 117 | "debug": "2.6.9", 118 | "depd": "~1.1.2", 119 | "http-errors": "1.7.2", 120 | "iconv-lite": "0.4.24", 121 | "on-finished": "~2.3.0", 122 | "qs": "6.7.0", 123 | "raw-body": "2.4.0", 124 | "type-is": "~1.6.17" 125 | } 126 | }, 127 | "buffer-equal-constant-time": { 128 | "version": "1.0.1", 129 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 130 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 131 | }, 132 | "bytes": { 133 | "version": "3.1.0", 134 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 135 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 136 | }, 137 | "content-disposition": { 138 | "version": "0.5.3", 139 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 140 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 141 | "requires": { 142 | "safe-buffer": "5.1.2" 143 | } 144 | }, 145 | "content-type": { 146 | "version": "1.0.4", 147 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 148 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 149 | }, 150 | "cookie": { 151 | "version": "0.4.0", 152 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 153 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 154 | }, 155 | "cookie-signature": { 156 | "version": "1.0.6", 157 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 158 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 159 | }, 160 | "cors": { 161 | "version": "2.8.5", 162 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 163 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 164 | "requires": { 165 | "object-assign": "^4", 166 | "vary": "^1" 167 | } 168 | }, 169 | "debug": { 170 | "version": "2.6.9", 171 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 172 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 173 | "requires": { 174 | "ms": "2.0.0" 175 | } 176 | }, 177 | "depd": { 178 | "version": "1.1.2", 179 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 180 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 181 | }, 182 | "destroy": { 183 | "version": "1.0.4", 184 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 185 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 186 | }, 187 | "dom-storage": { 188 | "version": "2.1.0", 189 | "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", 190 | "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==" 191 | }, 192 | "ecdsa-sig-formatter": { 193 | "version": "1.0.11", 194 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 195 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 196 | "requires": { 197 | "safe-buffer": "^5.0.1" 198 | } 199 | }, 200 | "ee-first": { 201 | "version": "1.1.1", 202 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 203 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 204 | }, 205 | "encodeurl": { 206 | "version": "1.0.2", 207 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 208 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 209 | }, 210 | "escape-html": { 211 | "version": "1.0.3", 212 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 213 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 214 | }, 215 | "etag": { 216 | "version": "1.8.1", 217 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 218 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 219 | }, 220 | "express": { 221 | "version": "4.17.1", 222 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 223 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 224 | "requires": { 225 | "accepts": "~1.3.7", 226 | "array-flatten": "1.1.1", 227 | "body-parser": "1.19.0", 228 | "content-disposition": "0.5.3", 229 | "content-type": "~1.0.4", 230 | "cookie": "0.4.0", 231 | "cookie-signature": "1.0.6", 232 | "debug": "2.6.9", 233 | "depd": "~1.1.2", 234 | "encodeurl": "~1.0.2", 235 | "escape-html": "~1.0.3", 236 | "etag": "~1.8.1", 237 | "finalhandler": "~1.1.2", 238 | "fresh": "0.5.2", 239 | "merge-descriptors": "1.0.1", 240 | "methods": "~1.1.2", 241 | "on-finished": "~2.3.0", 242 | "parseurl": "~1.3.3", 243 | "path-to-regexp": "0.1.7", 244 | "proxy-addr": "~2.0.5", 245 | "qs": "6.7.0", 246 | "range-parser": "~1.2.1", 247 | "safe-buffer": "5.1.2", 248 | "send": "0.17.1", 249 | "serve-static": "1.14.1", 250 | "setprototypeof": "1.1.1", 251 | "statuses": "~1.5.0", 252 | "type-is": "~1.6.18", 253 | "utils-merge": "1.0.1", 254 | "vary": "~1.1.2" 255 | } 256 | }, 257 | "finalhandler": { 258 | "version": "1.1.2", 259 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 260 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 261 | "requires": { 262 | "debug": "2.6.9", 263 | "encodeurl": "~1.0.2", 264 | "escape-html": "~1.0.3", 265 | "on-finished": "~2.3.0", 266 | "parseurl": "~1.3.3", 267 | "statuses": "~1.5.0", 268 | "unpipe": "~1.0.0" 269 | } 270 | }, 271 | "firebase-functions": { 272 | "version": "3.2.0", 273 | "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.2.0.tgz", 274 | "integrity": "sha512-v61CXYFSb53SdSSqwc/QhdBrR+H0bhwxSOIhKIYFFa2m5APUsuj8SrkAOBL2CfOJo3yk7+nuuWOtz16JFaXLxg==", 275 | "requires": { 276 | "@types/express": "^4.17.0", 277 | "cors": "^2.8.5", 278 | "express": "^4.17.1", 279 | "jsonwebtoken": "^8.5.1", 280 | "lodash": "^4.17.14" 281 | } 282 | }, 283 | "forwarded": { 284 | "version": "0.1.2", 285 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 286 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 287 | }, 288 | "fresh": { 289 | "version": "0.5.2", 290 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 291 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 292 | }, 293 | "http-errors": { 294 | "version": "1.7.2", 295 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 296 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 297 | "requires": { 298 | "depd": "~1.1.2", 299 | "inherits": "2.0.3", 300 | "setprototypeof": "1.1.1", 301 | "statuses": ">= 1.5.0 < 2", 302 | "toidentifier": "1.0.0" 303 | } 304 | }, 305 | "iconv-lite": { 306 | "version": "0.4.24", 307 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 308 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 309 | "requires": { 310 | "safer-buffer": ">= 2.1.2 < 3" 311 | } 312 | }, 313 | "inherits": { 314 | "version": "2.0.3", 315 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 316 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 317 | }, 318 | "ipaddr.js": { 319 | "version": "1.9.0", 320 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 321 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 322 | }, 323 | "jsonwebtoken": { 324 | "version": "8.5.1", 325 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", 326 | "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", 327 | "requires": { 328 | "jws": "^3.2.2", 329 | "lodash.includes": "^4.3.0", 330 | "lodash.isboolean": "^3.0.3", 331 | "lodash.isinteger": "^4.0.4", 332 | "lodash.isnumber": "^3.0.3", 333 | "lodash.isplainobject": "^4.0.6", 334 | "lodash.isstring": "^4.0.1", 335 | "lodash.once": "^4.0.0", 336 | "ms": "^2.1.1", 337 | "semver": "^5.6.0" 338 | }, 339 | "dependencies": { 340 | "ms": { 341 | "version": "2.1.2", 342 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 343 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 344 | } 345 | } 346 | }, 347 | "jwa": { 348 | "version": "1.4.1", 349 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", 350 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", 351 | "requires": { 352 | "buffer-equal-constant-time": "1.0.1", 353 | "ecdsa-sig-formatter": "1.0.11", 354 | "safe-buffer": "^5.0.1" 355 | } 356 | }, 357 | "jws": { 358 | "version": "3.2.2", 359 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 360 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 361 | "requires": { 362 | "jwa": "^1.4.1", 363 | "safe-buffer": "^5.0.1" 364 | } 365 | }, 366 | "lodash": { 367 | "version": "4.17.15", 368 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 369 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 370 | }, 371 | "lodash.includes": { 372 | "version": "4.3.0", 373 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 374 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 375 | }, 376 | "lodash.isboolean": { 377 | "version": "3.0.3", 378 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 379 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 380 | }, 381 | "lodash.isinteger": { 382 | "version": "4.0.4", 383 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 384 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 385 | }, 386 | "lodash.isnumber": { 387 | "version": "3.0.3", 388 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 389 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 390 | }, 391 | "lodash.isplainobject": { 392 | "version": "4.0.6", 393 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 394 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 395 | }, 396 | "lodash.isstring": { 397 | "version": "4.0.1", 398 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 399 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 400 | }, 401 | "lodash.once": { 402 | "version": "4.1.1", 403 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 404 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 405 | }, 406 | "media-typer": { 407 | "version": "0.3.0", 408 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 409 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 410 | }, 411 | "merge-descriptors": { 412 | "version": "1.0.1", 413 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 414 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 415 | }, 416 | "methods": { 417 | "version": "1.1.2", 418 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 419 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 420 | }, 421 | "mime": { 422 | "version": "1.6.0", 423 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 424 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 425 | }, 426 | "mime-db": { 427 | "version": "1.40.0", 428 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 429 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 430 | }, 431 | "mime-types": { 432 | "version": "2.1.24", 433 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 434 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 435 | "requires": { 436 | "mime-db": "1.40.0" 437 | } 438 | }, 439 | "ms": { 440 | "version": "2.0.0", 441 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 442 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 443 | }, 444 | "negotiator": { 445 | "version": "0.6.2", 446 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 447 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 448 | }, 449 | "object-assign": { 450 | "version": "4.1.1", 451 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 452 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 453 | }, 454 | "on-finished": { 455 | "version": "2.3.0", 456 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 457 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 458 | "requires": { 459 | "ee-first": "1.1.1" 460 | } 461 | }, 462 | "parseurl": { 463 | "version": "1.3.3", 464 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 465 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 466 | }, 467 | "path-to-regexp": { 468 | "version": "0.1.7", 469 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 470 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 471 | }, 472 | "proxy-addr": { 473 | "version": "2.0.5", 474 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 475 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 476 | "requires": { 477 | "forwarded": "~0.1.2", 478 | "ipaddr.js": "1.9.0" 479 | } 480 | }, 481 | "qs": { 482 | "version": "6.7.0", 483 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 484 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 485 | }, 486 | "range-parser": { 487 | "version": "1.2.1", 488 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 489 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 490 | }, 491 | "raw-body": { 492 | "version": "2.4.0", 493 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 494 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 495 | "requires": { 496 | "bytes": "3.1.0", 497 | "http-errors": "1.7.2", 498 | "iconv-lite": "0.4.24", 499 | "unpipe": "1.0.0" 500 | } 501 | }, 502 | "safe-buffer": { 503 | "version": "5.1.2", 504 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 505 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 506 | }, 507 | "safer-buffer": { 508 | "version": "2.1.2", 509 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 510 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 511 | }, 512 | "semver": { 513 | "version": "5.7.1", 514 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 515 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 516 | }, 517 | "send": { 518 | "version": "0.17.1", 519 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 520 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 521 | "requires": { 522 | "debug": "2.6.9", 523 | "depd": "~1.1.2", 524 | "destroy": "~1.0.4", 525 | "encodeurl": "~1.0.2", 526 | "escape-html": "~1.0.3", 527 | "etag": "~1.8.1", 528 | "fresh": "0.5.2", 529 | "http-errors": "~1.7.2", 530 | "mime": "1.6.0", 531 | "ms": "2.1.1", 532 | "on-finished": "~2.3.0", 533 | "range-parser": "~1.2.1", 534 | "statuses": "~1.5.0" 535 | }, 536 | "dependencies": { 537 | "ms": { 538 | "version": "2.1.1", 539 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 540 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 541 | } 542 | } 543 | }, 544 | "serve-static": { 545 | "version": "1.14.1", 546 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 547 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 548 | "requires": { 549 | "encodeurl": "~1.0.2", 550 | "escape-html": "~1.0.3", 551 | "parseurl": "~1.3.3", 552 | "send": "0.17.1" 553 | } 554 | }, 555 | "setprototypeof": { 556 | "version": "1.1.1", 557 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 558 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 559 | }, 560 | "statuses": { 561 | "version": "1.5.0", 562 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 563 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 564 | }, 565 | "toidentifier": { 566 | "version": "1.0.0", 567 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 568 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 569 | }, 570 | "tslib": { 571 | "version": "1.10.0", 572 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 573 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" 574 | }, 575 | "type-is": { 576 | "version": "1.6.18", 577 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 578 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 579 | "requires": { 580 | "media-typer": "0.3.0", 581 | "mime-types": "~2.1.24" 582 | } 583 | }, 584 | "unpipe": { 585 | "version": "1.0.0", 586 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 587 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 588 | }, 589 | "utils-merge": { 590 | "version": "1.0.1", 591 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 592 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 593 | }, 594 | "vary": { 595 | "version": "1.1.2", 596 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 597 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 598 | }, 599 | "xmlhttprequest": { 600 | "version": "1.8.0", 601 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 602 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" 603 | } 604 | } 605 | } 606 | --------------------------------------------------------------------------------