Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Feature authentication #35

Open
wants to merge 7 commits into
base: dockerized
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:5000/",
"eslintConfig": {
"extends": "react-app"
},
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@ import React, { useState } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import './App.css';

import PrivateRoute from './components/PrivateRoute';

import NavBar from './components/NavBar';
import Chart from './components/Chart';
import KarmaFeed from './components/KarmaFeed';
import UserProfile from './components/UserProfile';
import Login from './components/Login';

function App() {
const App = props => {

const [searchTerm, setSearchTerm] = useState('');

return (
<Router>
<NavBar search={searchTerm} onChange={value => setSearchTerm(value)} onClick={value => setSearchTerm(value)} />

<PrivateRoute path="/" component={NavBar} search={searchTerm} onChange={value => setSearchTerm(value)} onSearchClick={value => setSearchTerm(value)} />

<Switch>
<Route exact path="/" render={ props => (<Chart {...props} search={searchTerm} onClick={value => setSearchTerm(value)} />) } />
<Route exact path="/feed" render={ props => (<KarmaFeed {...props} search={searchTerm} onClick={value => setSearchTerm(value)} />) } />
<Route path="/user/:user">
<UserProfile search={searchTerm} />
</Route>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/" component={Chart} search={searchTerm} onSearchClick={value => setSearchTerm(value)} />
<PrivateRoute exact path="/feed" component={KarmaFeed} search={searchTerm} onSearchClick={value => setSearchTerm(value)} />
<PrivateRoute path="/user/:user" component={UserProfile} search={searchTerm} />
</Switch>

</Router>
);
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ const Chart = props => {
onChannelClick={ value => setChannel(value) }
onStartDateClick={ value => setStartDate(value) }
onEndDateClick={ value => setEndDate(value) }
onSearchClick={ value => props.onClick(value) }
onSearchClick={ value => value }
// onSearchClick={ value => props.onClick(value) }
onFilterClick={ value => setPaginationSearch(0) }
/>
: null
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/DateRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {
addMonths,
startOfWeek,
endOfWeek,
isSameDay,
differenceInCalendarDays,
// isSameDay,
// differenceInCalendarDays,
} from 'date-fns';
import { enGB } from 'date-fns/locale'
import 'react-date-range/dist/styles.css'; // main style file
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/KarmaFeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ const KarmaFeed = props => {
onChannelClick={ value => setChannel(value) }
onStartDateClick={ value => setStartDate(value) }
onEndDateClick={ value => setEndDate(value) }
onSearchClick={ value => props.onClick(value) }
onSearchClick={ value => value }
// onSearchClick={ value => props.onClick(value) }
onFilterClick={ value => handlePageClick({selected: value}) }
/>
: null
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/components/Login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';

const Login = props => {

return (
<div className="container pt-5 pb-5">
<div className="row justify-content-md-center">
<div className="col-3">
<a href="http://localhost:3000/auth/google/" rel="noopener noreferrer">Login with Google</a>
</div>
</div>
</div>
)

}

export default Login;
5 changes: 3 additions & 2 deletions frontend/src/components/NavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ const NavBar = props => {
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
<NavItem>
<Link to={"/" + urlParams} onClick={ e => props.onClick('') } className="nav-link">Top Chart</Link>
<Link to={"/" + urlParams} onClick={ value => props.onSearchClick('') } className="nav-link">Top Chart</Link>
</NavItem>
<NavItem>
<Link to={"/feed" + urlParams} onClick={ e => props.onClick('') } className="nav-link">Feed</Link>
<Link to={"/feed" + urlParams} onClick={ value => props.onSearchClick('') } className="nav-link">Feed</Link>

</NavItem>
</Nav>

Expand Down
40 changes: 40 additions & 0 deletions frontend/src/components/PrivateRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Route, Redirect } from 'react-router-dom';

const PrivateRoute = ({ component: Component, ...rest }) => {

const [isLogin, setIsLogin] = useState(false);
const [isActive, setIsActive] = useState(true);

useEffect(() => {

const fetchData = async() => {

try {
// eslint-disable-next-line
const result = await axios.get('/checkAuthentication');
if (isActive) setIsLogin(true);
} catch (err) {
if (isActive) setIsLogin(false);
} finally {
if (isActive) setIsActive(false);
}

}
fetchData();

}, [isLogin, isActive]);

if (isActive) return null;

return (
<Route {...rest} render={props => (
isLogin ?
<Component {...props} {...rest} search={rest.search} />
: <Redirect to={{ pathname: '/login' }} />
)} />
)
}

export default PrivateRoute;
5 changes: 5 additions & 0 deletions frontend/src/setupProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const proxy = require("http-proxy-middleware");

module.exports = function(app) {
app.use(proxy('/auth', { target: 'http://localhost:5000/' }));
};
102 changes: 99 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,31 @@
'use strict';
require( 'dotenv' ).config();
const app = require( './src/app' ),
slack = require( './src/slack' );
slack = require( './src/slack' ),
points = require( './src/points' );

const fs = require( 'fs' ),
mime = require( 'mime' ),
express = require( 'express' ),
passport = require('passport'),
cors = require( 'cors' ),
bodyParser = require( 'body-parser' ),
slackClient = require( '@slack/client' );
cookieParser = require( 'cookie-parser' ),
slackClient = require( '@slack/client' ),
session = require( 'express-session' ),
flash = require( 'connect-flash' );

const GoogleStrategy = require('passport-google-oauth20'); // require('passport-google-oauth20').Strategy;

/* eslint-disable no-process-env, no-magic-numbers */
const PORT = process.env.SCOREBOT_PORT || 80; // Let Heroku set the port.
const SLACK_OAUTH_ACCESS_TOKEN = process.env.SLACK_BOT_USER_OAUTH_ACCESS_TOKEN;
const protocol = process.env.SCOREBOT_USE_SSL !== '1' ? 'http://' : 'https://';
const frontendUrl = process.env.SCOREBOT_LEADERBOARD_URL;
const FRONTEND_URL = protocol + frontendUrl;
const google_id = process.env.GOOGLE_CLIENT_ID;
const google_secret = process.env.GOOGLE_CLIENT_SECRET;

/* eslint-enable no-process-env, no-magic-numbers */

/**
Expand All @@ -43,13 +53,94 @@ const bootstrap = ( options = {}) => {
const server = options.express || express();
slack.setSlackClient( options.slack || new slackClient.WebClient( SLACK_OAUTH_ACCESS_TOKEN ) );

passport.serializeUser((user, cb) => {
cb(null, user);
});

passport.deserializeUser((user, cb) => {
cb(null, user);
});

passport.use(new GoogleStrategy({
clientID: google_id,
clientSecret: google_secret,
callbackURL: "/auth/google/callback"
},
async(accessToken, refreshToken, profile, done) => {
try {
const verifyEmail = await points.verifyEmail( profile._json.email );

// Users that are in the db can login:
// if (verifyEmail) {
// return done(null, profile._json.email);
// } else {
// return done(null);
// }

return done(null, profile._json.email);

} catch ( err ) {
console.error( err.message );
}
}
));

server.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", FRONTEND_URL); // update to match the domain you will make the request from
// res.header("Access-Control-Allow-Origin", '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});

server.use( bodyParser.json() );

// let access_token;

server.use(cors());
// server.use(cors({
// 'allowedHeaders': ['sessionId', 'Content-Type'],
// 'exposedHeaders': ['sessionId'],
// 'methods': ['GET', 'POST'],
// 'credentials': true,
// 'origin': ['http://127.0.0.1:3000', 'http://localhost:3000', 'http://localhost:5000'], // here goes Frontend IP
// }));

server.use(cookieParser());
server.use(session({
secret: 'secret',
cookie:{_expires : (480 * 60 * 1000)}, // 8 hours
saveUninitialized: false,
resave: true,
unset: 'destroy'
}));
server.use(passport.initialize());
server.use(passport.session());

server.get('/auth/google',
passport.authenticate('google', { scope: ['profile', "email"], accessType: 'offline', approvalPrompt: 'force' }));

server.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect home.
res.redirect('http://localhost:3000/');
}
);

server.get("/checkAuthentication", (req, res, next) => {

console.log("IS AUTH: " + req.isAuthenticated());

if (req.isAuthenticated()) {
res.sendStatus(200);
return next();
}

res.sendStatus(403);
});

server.use(bodyParser.json());

server.enable( 'trust proxy' );
server.get( '/', app.handleGet );
server.post( '/', app.handlePost );
Expand All @@ -70,6 +161,11 @@ const bootstrap = ( options = {}) => {
server.get( '/karmafeed', app.handleGet );
server.get( '/userprofile', app.handleGet );

server.get( '/logout', (req, res) => {
req.logout();
res.redirect('http://localhost:3000/login');
});

return server.listen( PORT, () => {
console.log( 'Listening on port ' + PORT + '.' );
});
Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,24 @@
],
"dependencies": {
"@slack/client": "^4.3.1",
"bcrypt": "^5.0.0",
"body-parser": "^1.18.3",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.16.3",
"express-session": "^1.17.1",
"handlebars": "^4.7.6",
"http-proxy-middleware": "^1.0.6",
"jsonwebtoken": "^8.5.1",
"lodash.camelcase": "^4.3.0",
"mime": "^2.3.1",
"moment": "^2.29.1",
"mysql": "^2.18.1",
"passport": "^0.4.1",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.0",
"uuid": "^8.3.1"
},
"devDependencies": {
Expand Down
23 changes: 21 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

const events = require( './events' ),
helpers = require( './helpers' ),
leaderboard = require( './leaderboard' );
leaderboard = require( './leaderboard' ),
passport = require('passport');

// eslint-disable-next-line no-process-env
const SLACK_VERIFICATION_TOKEN = process.env.SLACK_VERIFICATION_TOKEN;
Expand Down Expand Up @@ -88,7 +89,13 @@ const handleGet = async( request, response ) => {
// provided - the full link can be retrieved by requesting the leaderboard within Slack.
// TODO: This should probably be split out into a separate function of sorts, like handlePost.
case '/leaderboard':
response.json( await leaderboard.getForWeb( request ) );
// response.send("TEST");
// response.json( await leaderboard.getForWeb( request ) );
try {
response.json( await leaderboard.getForWeb( request ) );
} catch(err) {
console.log(err.message);
}
break;

case '/channels':
Expand Down Expand Up @@ -117,6 +124,18 @@ const handleGet = async( request, response ) => {

// A simple default GET response is sometimes useful for troubleshooting.
default:

// console.log(JSON.stringify(request.cookies));
// response.writeHead(200, {
// "Set-Cookie": "token=" + request.cookies + "; HttpOnly",
// "Access-Control-Allow-Credentials": "true"
// }).send();
// response.send(response);

// console.log('Cookies: ', request.cookies);
// console.log('Signed Cookies: ', request.signedCookies);
// console.log("SESSION: " + JSON.stringify(request.session));

response.send( 'It works! However, this app only accepts POST requests for now.' );
break;

Expand Down
Loading