Skip to content

Commit

Permalink
Feat/13-authentication (#26)
Browse files Browse the repository at this point in the history
* Set refresh token in /login route

Also adds JWT_SECRET

* Remove code that sets/gets token in localStorage

* Fix dep cycle, refactor

* Create auth router for login in, refresh, logout

- Refactor code to reflect new route

* Refactor

* Return username from refresh endpoint

* Set up refresh logic in frontend

- Persists the logged in state by making request to refresh
endpoint every page reload

- Shows new loading view while that request is being made

* Refactor Login RTL test

* Add HTTP interceptor to catch 401s and refetch access token

* Add test for App.tsx refresh request

* Replace real store with test store in tests

* Change expiration time of tokens

* Prefetch getUsers in App.tsx instead of index.tsx

* Add logout button on mobile, refactor

* Fix bug related to updating username

* Fix broken backend tests

* Refactor tests for better scope/clarity and fix broken tests after adding refresh token feature

* Add tests for logging out

* Refactor cypress commands
  • Loading branch information
lyncasterc authored Mar 20, 2024
1 parent 108ab48 commit 356ae7f
Show file tree
Hide file tree
Showing 60 changed files with 1,803 additions and 922 deletions.
1 change: 1 addition & 0 deletions backend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
'no-console': 0,
'no-underscore-dangle': ['error', { 'allow': [ '_id', '__v'] }],
'no-param-reassign': ['error', { 'props': false }],
'curly': 'error',
},
ignorePatterns: [
'.eslintrc.js',
Expand Down
56 changes: 56 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@types/express": "^4.17.13",
"bcrypt": "^5.1.0",
"cloudinary": "^1.29.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"dotenv": "^16.0.0",
Expand All @@ -34,6 +35,7 @@
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.12",
"@types/jest": "^27.4.1",
"@types/jsonwebtoken": "^8.5.8",
Expand Down
2 changes: 1 addition & 1 deletion backend/requests/post-requests.rest
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Content-Type: application/json

###
#log in
POST http://localhost:3001/api/login
POST http://localhost:3001/api/auth/login
Content-Type: application/json

{
Expand Down
2 changes: 1 addition & 1 deletion backend/requests/put-requests.rest
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Content-Type: application/json
###
#log in first user
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOiI2NWU0ZjZhZTNjMjg4Y2EzODVkYzJhNGEiLCJpYXQiOjE3MDk2MTEzMTAsImV4cCI6MTcwOTYxNDkxMH0.gmUZvHrjxO8ceZVOm0kTh3fYboJldmsCdxMPMyUmbH4
POST http://localhost:3001/api/login
POST http://localhost:3001/api/auth/login
Content-Type: application/json

{
Expand Down
11 changes: 8 additions & 3 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import express from 'express';
import cors from 'cors';
import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import mongodbConnect, { testMongodb } from './mongo';
import config from './utils/config';
import postRouter from './routes/posts';
import userRouter from './routes/users';
import loginRouter from './routes/login';
import authRouter from './routes/auth';
import testRouter from './routes/tests';
import likeRouter from './routes/likes';
import { errorHandler } from './utils/middleware';
Expand All @@ -25,6 +26,7 @@ app.get('/health', (_req, res) => {
res.send('ok');
});

app.use(cookieParser());
app.use(cors());
app.use(express.static('build'));
app.use(express.json({ limit: '50mb' }));
Expand All @@ -33,9 +35,12 @@ app.use(morgan('dev'));

app.use('/api/posts', postRouter);
app.use('/api/users', userRouter);
app.use('/api/login', loginRouter);
app.use('/api/auth', authRouter);
app.use('/api/likes', likeRouter);

if (NODE_ENV !== 'production') app.use('/api/test', testRouter);
if (NODE_ENV !== 'production') {
app.use('/api/test', testRouter);
}

app.use(errorHandler);
export default app;
137 changes: 137 additions & 0 deletions backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import express from 'express';
import fieldParsers from '../utils/field-parsers';
import logger from '../utils/logger';
import { User } from '../mongo';
import config from '../utils/config';

const router = express.Router();
const { JWT_SECRET } = config;

router.post('/login', async (req, res) => {
if (!JWT_SECRET) {
return res.status(500).send({ error: 'JWT_SECRET is not set.' });
}

const { body } = req;
let loginFields: {
username: string,
password: string,
};

try {
loginFields = fieldParsers.proofLogInFields(body);
} catch (error) {
return res.status(400).send({ error: logger.getErrorMessage(error) });
}

try {
const user = await User.findOne({ username: loginFields.username });
const isUserValidated = !user
? false
: await bcrypt.compare(loginFields.password, user.passwordHash);

if (!isUserValidated) {
return res.status(401).send({
error: 'Invalid username or password.',
});
}

const userTokenInfo = {
username: user.username,
id: user.id,
};

const accessToken = jwt.sign(
userTokenInfo,
JWT_SECRET,
{ expiresIn: '15m' },
);
const refreshToken = jwt.sign(
userTokenInfo,
JWT_SECRET,
{ expiresIn: '30d' },
);

res.cookie('refreshToken', refreshToken, {
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
});

return res.status(200).send({ accessToken, username: user.username });
} catch (error) {
return res.status(500).send({ error: 'Something went wrong!' });
}
});

router.post('/refresh', async (req, res, next) => {
if (!JWT_SECRET) {
return res.status(500).send({ error: 'JWT_SECRET is not set.' });
}

let decodedRefreshToken;
const { refreshToken } = req.cookies;

if (refreshToken) {
try {
decodedRefreshToken = jwt.verify(
refreshToken,
JWT_SECRET,
) as jwt.JwtPayload;
} catch (error) {
res.clearCookie('refreshToken');

return next(error);
}
}

if (
!decodedRefreshToken
|| !decodedRefreshToken.id
|| !decodedRefreshToken.username
) {
res.clearCookie('refreshToken');

return res.status(401).send({
error: 'refresh token missing or invalid.',
});
}

const user = await User.findById(decodedRefreshToken.id);

if (!user) {
res.clearCookie('refreshToken');

return res.status(401).send({
error: 'refresh token is not valid for any user.',
});
}

// generate new access token
const userTokenInfo = {
username: user.username,
id: decodedRefreshToken.id,
};

const newAccessToken = jwt.sign(
userTokenInfo,
JWT_SECRET,
{ expiresIn: '15m' },
);

return res.status(200).send(
{
accessToken: newAccessToken,
username: user.username,
},
);
});

router.post('/logout', (_req, res) => {
res.clearCookie('refreshToken');
res.status(204).end();
});

export default router;
53 changes: 0 additions & 53 deletions backend/src/routes/login.ts
Original file line number Diff line number Diff line change
@@ -1,53 +0,0 @@
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import express from 'express';
import fieldParsers from '../utils/field-parsers';
import logger from '../utils/logger';
import { User } from '../mongo';

const router = express.Router();

router.post('/', async (req, res) => {
const { body } = req;
let loginFields: {
username: string,
password: string,
};

try {
loginFields = fieldParsers.proofLogInFields(body);
} catch (error) {
return res.status(400).send({ error: logger.getErrorMessage(error) });
}

try {
// TODO: populate fields? frontend needs the image and posts of the logged in user.
const user = await User.findOne({ username: loginFields.username });
const isUserValidated = !user
? false
: await bcrypt.compare(loginFields.password, user.passwordHash);

if (!isUserValidated) {
return res.status(401).send({
error: 'Invalid username or password.',
});
}

const userTokenInfo = {
username: user.username,
id: user.id,
};

const token = jwt.sign(
userTokenInfo,
process.env.SECRET = 'scrt',
{ expiresIn: 60 * 60 },
);

return res.status(200).send({ token, username: user.username });
} catch (error) {
return res.status(500).send({ error: 'Something went wrong!' });
}
});

export default router;
2 changes: 2 additions & 0 deletions backend/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
CLOUDINARY_NAME,
CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET,
JWT_SECRET,
} = process.env;

export default {
Expand All @@ -14,4 +15,5 @@ export default {
CLOUDINARY_NAME,
CLOUDINARY_API_KEY,
CLOUDINARY_API_SECRET,
JWT_SECRET,
};
Loading

0 comments on commit 356ae7f

Please sign in to comment.