diff --git a/functions/handlers/users.js b/functions/handlers/users.js index cdeb3f6..21f9b96 100644 --- a/functions/handlers/users.js +++ b/functions/handlers/users.js @@ -121,11 +121,11 @@ exports.login = (req, res) => { return data.user.getIdToken(); }) .then((token) => { - return res.json({ token }); + return res.status(200).json({ token }); }) .catch((err) => { console.error(err); - if (err.code === "auth/wrong-password") { + if (err.code === "auth/wrong-password" || err.code === "auth/invalid-email") { return res .status(403) .json({ general: "Invalid credentials. Please try again." }); @@ -214,3 +214,20 @@ exports.getUserDetails = (req, res) => { return res.status(500).json({ error: err.code }); }); }; + +exports.getAuthenticatedUser = (req, res) => { + let userData = {}; + db.doc(`/users/${req.user.handle}`) + .get() + .then((doc) => { + if (doc.exists) { + userData.credentials = doc.data(); + return res.status(200).json({userData}); + } else { + return res.status(400).json({error: "User not found."}) + }}) + .catch((err) => { + console.error(err); + return res.status(500).json({ error: err.code }); + }); +}; diff --git a/functions/index.js b/functions/index.js index eb99cbf..5c728bc 100644 --- a/functions/index.js +++ b/functions/index.js @@ -10,6 +10,7 @@ app.use(cors()); * handlers/users.js * *------------------------------------------------------------------*/ const { + getAuthenticatedUser, getUserDetails, getProfileInfo, login, @@ -34,6 +35,8 @@ app.get("/getProfileInfo", fbAuth, getProfileInfo); // Updates the currently logged in user's profile information app.post("/updateProfileInfo", fbAuth, updateProfileInfo); +app.get("/user", fbAuth, getAuthenticatedUser); + /*------------------------------------------------------------------* * handlers/post.js * *------------------------------------------------------------------*/ diff --git a/functions/util/fbAuth.js b/functions/util/fbAuth.js index a9243cc..3b59d14 100644 --- a/functions/util/fbAuth.js +++ b/functions/util/fbAuth.js @@ -4,6 +4,13 @@ const { admin, db } = require('./admin'); // The function will only execute if the user is logged in, or rather, they have // a valid token module.exports = (req, res, next) => { + console.log(req); + console.log(req.body); + console.log(req.headers); + console.log(req.headers.authorization); + console.log(JSON.stringify(req.body)); + console.log(JSON.stringify(req.header)); + let idToken; // Checking that the token exists in the header of the request diff --git a/twistter-frontend/package-lock.json b/twistter-frontend/package-lock.json index f7ffc91..1e60fab 100644 --- a/twistter-frontend/package-lock.json +++ b/twistter-frontend/package-lock.json @@ -5277,6 +5277,11 @@ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=" }, + "jwt-decode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz", + "integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/twistter-frontend/package.json b/twistter-frontend/package.json index 003bf65..f03c528 100644 --- a/twistter-frontend/package.json +++ b/twistter-frontend/package.json @@ -10,6 +10,7 @@ "clsx": "^1.0.4", "create-react-app": "^3.1.2", "install": "^0.13.0", + "jwt-decode": "^2.2.0", "node-pre-gyp": "^0.13.0", "react": "^16.9.0", "react-dom": "^16.9.0", diff --git a/twistter-frontend/src/App.js b/twistter-frontend/src/App.js index ccbff92..8e18621 100644 --- a/twistter-frontend/src/App.js +++ b/twistter-frontend/src/App.js @@ -1,29 +1,49 @@ /* eslint-disable */ -import React, { Component } from 'react'; +import React, { Component } from "react"; +import "./App.css"; -import './App.css'; +import { BrowserRouter as Router } from "react-router-dom"; +import Route from "react-router-dom/Route"; +import Navbar from "./components/layout/NavBar"; +import jwtDecode from "jwt-decode"; + +// Redux +import { Provider } from "react-redux"; +import store from "./redux/store"; import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider'; import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; - -import { BrowserRouter as Router } from 'react-router-dom'; -import Route from 'react-router-dom/Route'; -import Navbar from './components/layout/NavBar'; import themeObject from './util/theme'; +// Pages import home from './pages/Home'; import register from './pages/Register'; import login from './pages/Login'; import user from './pages/user'; +import writeMicroblog from "./Writing_Microblogs.js"; +import edit from "./pages/edit.js"; +import userLine from "./Userline.js"; -import writeMicroblog from './Writing_Microblogs.js'; -import edit from './pages/edit.js'; -import userLine from './Userline.js'; +// Components +import AuthRoute from "./util/AuthRoute"; + +let authenticated; +const token = localStorage.FBIdToken; +if (token) { + const decodedToken = jwtDecode(token); + if (decodedToken.exp * 1000 < Date.now()) { + window.location.href = "/login"; + authenticated = false; + } else { + authenticated = true; + } +} const theme = createMuiTheme(themeObject); class App extends Component { render() { return ( +
@@ -41,6 +61,7 @@ class App extends Component { + ); } } diff --git a/twistter-frontend/src/index.js b/twistter-frontend/src/index.js index 5b9a1ed..f722945 100644 --- a/twistter-frontend/src/index.js +++ b/twistter-frontend/src/index.js @@ -1,8 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import * as serviceWorker from './serviceWorker'; ReactDOM.render( , document.getElementById('root') -); \ No newline at end of file +); + +serviceWorker.unregister(); \ No newline at end of file diff --git a/twistter-frontend/src/pages/Login.js b/twistter-frontend/src/pages/Login.js index 2b671ce..9492372 100644 --- a/twistter-frontend/src/pages/Login.js +++ b/twistter-frontend/src/pages/Login.js @@ -5,69 +5,176 @@ import axios from 'axios'; import PropTypes from 'prop-types'; import logo from '../images/twistter-logo.png'; -import TextField from '@material-ui/core/TextField'; -class Login extends Component { +// Material-UI stuff +import Button from "@material-ui/core/Button"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Grid from "@material-ui/core/Grid"; +import TextField from "@material-ui/core/TextField"; +import Typography from "@material-ui/core/Typography"; +import withStyles from "@material-ui/core/styles/withStyles"; + +// Redux stuff +import { connect } from 'react-redux'; +import { loginUser } from '../redux/actions/userActions'; + +const styles = { + form: { + textAlign: "center" + }, + textField: { + marginBottom: 30 + }, + pageTitle: { + // marginTop: 20, + marginBottom: 40 + }, + button: { + positon: "relative", + marginBottom: 30 + }, + progress: { + position: "absolute" + } +}; + +export class Login extends Component { + // componentDidMount() { + // axios + // .get("/getProfileInfo") + // .then((res) => { + // this.setState({ + // firstName: res.data.firstName, + // lastName: res.data.lastName, + // email: res.data.email, + // handle: res.data.handle, + // bio: res.data.bio + // }); + // }) + // .catch((err) => { + // console.error(err); + // }); + // } + + // Constructor for the state constructor() { super(); this.state = { - email: '', - password: '', - errors: {} + email: "", + password:"", + errors: {} }; - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); -}; -handleSubmit = (event) => { - event.preventDefault(); - const userData = { - email: this.state.email, - password: this.state.password + } + + componentWillReceiveProps(nextProps) { + if (nextProps.UI.errors) { + this.setState({ errors: nextProps.UI.errors }); + } + } + + // Runs whenever the submit button is clicked. + // Updates the database entry of the signed in user with the + // data stored in the state. + handleSubmit = (event) => { + event.preventDefault(); + const loginData = { + email: this.state.email, + password: this.state.password, }; - axios.post('http://localhost:5001/twistter-e4649/us-central1/api/login', userData) - .then(res => { - console.log(res.data); - localStorage.setItem('firebaseIdToken', `Bearer ${res.data.token}`); - this.props.history.push('/home'); - }) - .catch(err => { - this.setState({ - errors: err.response.data - }); - }); -}; -handleChange = (event) => { + this.props.loginUser(loginData, this.props.history); + }; + + // Updates the state whenever one of the textboxes changes. + // The key is the name of the textbox and the value is the + // value in the text box. + handleChange = (event) => { this.setState({ - [event.target.name]: event.target.value + [event.target.name]: event.target.value, + errors: { + [event.target.name]: null + } }); -}; + }; render() { - const { classes } = this.props; + const { classes, UI: { loading } } = this.props; const { errors } = this.state; - return ( -
- logo -

- Log in to Twistter -

-
- -

- -

- - -
+ return ( + + + + logo + + Log in to Twistter + +
+ + + + {errors.general && ( + Wrong Email or Password + )} + +
+ + ); - }; + } +} + +Login.propTypes = { + classes: PropTypes.object.isRequired, + loginUser: PropTypes.func.isRequired, + user: PropTypes.object.isRequired, + UI: PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => ({ + user: state.user, + UI: state.UI, +}); + +const mapActionsToProps = { + loginUser } Login.propTypes = { classes: PropTypes.object.isRequired }; -export default Login; \ No newline at end of file +export default connect(mapStateToProps, mapActionsToProps)(withStyles(styles)(Login)); diff --git a/twistter-frontend/src/pages/edit.js b/twistter-frontend/src/pages/edit.js index 306e19b..14d5172 100644 --- a/twistter-frontend/src/pages/edit.js +++ b/twistter-frontend/src/pages/edit.js @@ -85,6 +85,7 @@ export class edit extends Component { axios .post("/updateProfileInfo", newProfileData) .then((res) => { + console.log(res); this.setState({ loading: false }); @@ -92,6 +93,7 @@ export class edit extends Component { // TODO: Need to redirect user to their profile page }) .catch((err) => { + console.log(err); this.setState({ errors: err.response.data, loading: false diff --git a/twistter-frontend/src/redux/actions/dataActions.js b/twistter-frontend/src/redux/actions/dataActions.js new file mode 100644 index 0000000..e69de29 diff --git a/twistter-frontend/src/redux/actions/userActions.js b/twistter-frontend/src/redux/actions/userActions.js new file mode 100644 index 0000000..cc6eb56 --- /dev/null +++ b/twistter-frontend/src/redux/actions/userActions.js @@ -0,0 +1,36 @@ +import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI} from '../types'; +import axios from 'axios'; + + +export const getUserData = () => (dispatch) => { + axios.get('/user') + .then((res) => { + dispatch({ + type: SET_USER, + payload: res.data, + }) + }) + .catch((err) => console.error(err)); +} + +export const loginUser = (loginData, history) => (dispatch) => { + dispatch({ type: LOADING_UI }); + axios + .post("/login", loginData) + .then((res) => { + // Save the login token + const FBIdToken = `Bearer ${res.data.token}`; + localStorage.setItem('FBIdToken', FBIdToken); + axios.defaults.headers.common['Authorization'] = FBIdToken; + dispatch(getUserData()); + dispatch({ type: CLEAR_ERRORS }) + // Redirects to home page + history.push('/home'); + }) + .catch((err) => { + dispatch ({ + type: SET_ERRORS, + payload: err.response.data, + }) + }); +} \ No newline at end of file diff --git a/twistter-frontend/src/redux/reducers/dataReducer.js b/twistter-frontend/src/redux/reducers/dataReducer.js new file mode 100644 index 0000000..e69de29 diff --git a/twistter-frontend/src/redux/reducers/uiReducer.js b/twistter-frontend/src/redux/reducers/uiReducer.js new file mode 100644 index 0000000..65e781d --- /dev/null +++ b/twistter-frontend/src/redux/reducers/uiReducer.js @@ -0,0 +1,30 @@ +import { SET_ERRORS, CLEAR_ERRORS, LOADING_UI } from '../types'; + +const initialState = { + loading: false, + errors: null +}; + +export default function(state = initialState, action) { + switch(action.type) { + case SET_ERRORS: + return { + ...state, + loading: false, + errors: action.payload + }; + case CLEAR_ERRORS: + return { + ...state, + loading: false, + errors: null + }; + case LOADING_UI: + return { + ...state, + loading: true + } + default: + return state; + } +} \ No newline at end of file diff --git a/twistter-frontend/src/redux/reducers/userReducer.js b/twistter-frontend/src/redux/reducers/userReducer.js new file mode 100644 index 0000000..7a29e90 --- /dev/null +++ b/twistter-frontend/src/redux/reducers/userReducer.js @@ -0,0 +1,28 @@ +import {SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from '../types'; + +const initialState = { + authenticated: false, + credentials: {}, + likes: [], + notifications: [] +}; + +export default function(state = initialState, action) { + switch(action.type) { + case SET_AUTHENTICATED: + return { + ...state, + authenticated: true, + + }; + case SET_UNAUTHENTICATED: + return initialState; + case SET_USER: + return { + authenticated: true, + ...action.payload, + }; + default: + return state; + } +} \ No newline at end of file diff --git a/twistter-frontend/src/redux/store.js b/twistter-frontend/src/redux/store.js new file mode 100644 index 0000000..b79ebe6 --- /dev/null +++ b/twistter-frontend/src/redux/store.js @@ -0,0 +1,27 @@ +import { createStore, combineReducers, applyMiddleware, compose } from "redux"; +import thunk from "redux-thunk"; + +import userReducer from "./reducers/userReducer"; +import dataReducer from "./reducers/dataReducer"; +import uiReducer from "./reducers/uiReducer"; + +const initialState = {}; + +const middleWare = [thunk]; + +const reducers = combineReducers({ + user: userReducer, + data: dataReducer, + UI: uiReducer +}); + +const store = createStore( + reducers, + initialState, + compose( + applyMiddleware(...middleWare), + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() + ) +); + +export default store; diff --git a/twistter-frontend/src/redux/types.js b/twistter-frontend/src/redux/types.js new file mode 100644 index 0000000..6f16afc --- /dev/null +++ b/twistter-frontend/src/redux/types.js @@ -0,0 +1,12 @@ +// User reducer types +export const SET_AUTHENTICATED = 'SET_AUTHENTICATED'; +export const SET_UNAUTHENTICATED = 'SET_UNAUTHENTICATED'; +export const SET_USER = 'SET_USER'; +export const LOADING_USER = 'LOADING_USER'; + +// UI reducer types +export const SET_ERRORS = 'SET_ERRORS'; +export const LOADING_UI = 'LOADING_UI'; +export const CLEAR_ERRORS = 'CLEAR_ERRORS'; + +// Data reducer types \ No newline at end of file diff --git a/twistter-frontend/src/serviceWorker.js b/twistter-frontend/src/serviceWorker.js new file mode 100644 index 0000000..b685ab1 --- /dev/null +++ b/twistter-frontend/src/serviceWorker.js @@ -0,0 +1,135 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://bit.ly/CRA-PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.1/8 is considered localhost for IPv4. + window.location.hostname.match( + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + ) + ); + + export function register(config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://bit.ly/CRA-PWA' + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } + } + + function registerValidSW(swUrl, config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' + ); + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.'); + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error('Error during service worker registration:', error); + }); + } + + function checkValidServiceWorker(swUrl, config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type'); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf('javascript') === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + 'No internet connection found. App is running in offline mode.' + ); + }); + } + + export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } + } diff --git a/twistter-frontend/src/util/AuthRoute.js b/twistter-frontend/src/util/AuthRoute.js new file mode 100644 index 0000000..309ccf0 --- /dev/null +++ b/twistter-frontend/src/util/AuthRoute.js @@ -0,0 +1,22 @@ +import React from 'react' +import { Route, Redirect} from 'react-router-dom'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +const AuthRoute = ({ component: Component, authenticated, ...rest }) => ( + + authenticated === true ? : } + /> +); + +const mapStateToProps = (state) => ({ + authenticated: state.user.authenticated +}); + +AuthRoute.propTypes = { + user: PropTypes.object +} + +export default connect(mapStateToProps)(AuthRoute);