Skip to content

Commit 356ae7f

Browse files
authored
Feat/13-authentication (#26)
* 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
1 parent 108ab48 commit 356ae7f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1803
-922
lines changed

backend/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = {
2121
'no-console': 0,
2222
'no-underscore-dangle': ['error', { 'allow': [ '_id', '__v'] }],
2323
'no-param-reassign': ['error', { 'props': false }],
24+
'curly': 'error',
2425
},
2526
ignorePatterns: [
2627
'.eslintrc.js',

backend/package-lock.json

+56
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@types/express": "^4.17.13",
2323
"bcrypt": "^5.1.0",
2424
"cloudinary": "^1.29.0",
25+
"cookie-parser": "^1.4.6",
2526
"cors": "^2.8.5",
2627
"cross-env": "^7.0.3",
2728
"dotenv": "^16.0.0",
@@ -34,6 +35,7 @@
3435
},
3536
"devDependencies": {
3637
"@types/bcrypt": "^5.0.0",
38+
"@types/cookie-parser": "^1.4.7",
3739
"@types/cors": "^2.8.12",
3840
"@types/jest": "^27.4.1",
3941
"@types/jsonwebtoken": "^8.5.8",

backend/requests/post-requests.rest

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Content-Type: application/json
1111

1212
###
1313
#log in
14-
POST http://localhost:3001/api/login
14+
POST http://localhost:3001/api/auth/login
1515
Content-Type: application/json
1616

1717
{

backend/requests/put-requests.rest

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Content-Type: application/json
2424
###
2525
#log in first user
2626
@token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOiI2NWU0ZjZhZTNjMjg4Y2EzODVkYzJhNGEiLCJpYXQiOjE3MDk2MTEzMTAsImV4cCI6MTcwOTYxNDkxMH0.gmUZvHrjxO8ceZVOm0kTh3fYboJldmsCdxMPMyUmbH4
27-
POST http://localhost:3001/api/login
27+
POST http://localhost:3001/api/auth/login
2828
Content-Type: application/json
2929

3030
{

backend/src/app.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import express from 'express';
22
import cors from 'cors';
33
import morgan from 'morgan';
4+
import cookieParser from 'cookie-parser';
45
import mongodbConnect, { testMongodb } from './mongo';
56
import config from './utils/config';
67
import postRouter from './routes/posts';
78
import userRouter from './routes/users';
8-
import loginRouter from './routes/login';
9+
import authRouter from './routes/auth';
910
import testRouter from './routes/tests';
1011
import likeRouter from './routes/likes';
1112
import { errorHandler } from './utils/middleware';
@@ -25,6 +26,7 @@ app.get('/health', (_req, res) => {
2526
res.send('ok');
2627
});
2728

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

3436
app.use('/api/posts', postRouter);
3537
app.use('/api/users', userRouter);
36-
app.use('/api/login', loginRouter);
38+
app.use('/api/auth', authRouter);
3739
app.use('/api/likes', likeRouter);
3840

39-
if (NODE_ENV !== 'production') app.use('/api/test', testRouter);
41+
if (NODE_ENV !== 'production') {
42+
app.use('/api/test', testRouter);
43+
}
44+
4045
app.use(errorHandler);
4146
export default app;

backend/src/routes/auth.ts

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import jwt from 'jsonwebtoken';
2+
import bcrypt from 'bcrypt';
3+
import express from 'express';
4+
import fieldParsers from '../utils/field-parsers';
5+
import logger from '../utils/logger';
6+
import { User } from '../mongo';
7+
import config from '../utils/config';
8+
9+
const router = express.Router();
10+
const { JWT_SECRET } = config;
11+
12+
router.post('/login', async (req, res) => {
13+
if (!JWT_SECRET) {
14+
return res.status(500).send({ error: 'JWT_SECRET is not set.' });
15+
}
16+
17+
const { body } = req;
18+
let loginFields: {
19+
username: string,
20+
password: string,
21+
};
22+
23+
try {
24+
loginFields = fieldParsers.proofLogInFields(body);
25+
} catch (error) {
26+
return res.status(400).send({ error: logger.getErrorMessage(error) });
27+
}
28+
29+
try {
30+
const user = await User.findOne({ username: loginFields.username });
31+
const isUserValidated = !user
32+
? false
33+
: await bcrypt.compare(loginFields.password, user.passwordHash);
34+
35+
if (!isUserValidated) {
36+
return res.status(401).send({
37+
error: 'Invalid username or password.',
38+
});
39+
}
40+
41+
const userTokenInfo = {
42+
username: user.username,
43+
id: user.id,
44+
};
45+
46+
const accessToken = jwt.sign(
47+
userTokenInfo,
48+
JWT_SECRET,
49+
{ expiresIn: '15m' },
50+
);
51+
const refreshToken = jwt.sign(
52+
userTokenInfo,
53+
JWT_SECRET,
54+
{ expiresIn: '30d' },
55+
);
56+
57+
res.cookie('refreshToken', refreshToken, {
58+
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
59+
httpOnly: true,
60+
secure: process.env.NODE_ENV === 'production',
61+
});
62+
63+
return res.status(200).send({ accessToken, username: user.username });
64+
} catch (error) {
65+
return res.status(500).send({ error: 'Something went wrong!' });
66+
}
67+
});
68+
69+
router.post('/refresh', async (req, res, next) => {
70+
if (!JWT_SECRET) {
71+
return res.status(500).send({ error: 'JWT_SECRET is not set.' });
72+
}
73+
74+
let decodedRefreshToken;
75+
const { refreshToken } = req.cookies;
76+
77+
if (refreshToken) {
78+
try {
79+
decodedRefreshToken = jwt.verify(
80+
refreshToken,
81+
JWT_SECRET,
82+
) as jwt.JwtPayload;
83+
} catch (error) {
84+
res.clearCookie('refreshToken');
85+
86+
return next(error);
87+
}
88+
}
89+
90+
if (
91+
!decodedRefreshToken
92+
|| !decodedRefreshToken.id
93+
|| !decodedRefreshToken.username
94+
) {
95+
res.clearCookie('refreshToken');
96+
97+
return res.status(401).send({
98+
error: 'refresh token missing or invalid.',
99+
});
100+
}
101+
102+
const user = await User.findById(decodedRefreshToken.id);
103+
104+
if (!user) {
105+
res.clearCookie('refreshToken');
106+
107+
return res.status(401).send({
108+
error: 'refresh token is not valid for any user.',
109+
});
110+
}
111+
112+
// generate new access token
113+
const userTokenInfo = {
114+
username: user.username,
115+
id: decodedRefreshToken.id,
116+
};
117+
118+
const newAccessToken = jwt.sign(
119+
userTokenInfo,
120+
JWT_SECRET,
121+
{ expiresIn: '15m' },
122+
);
123+
124+
return res.status(200).send(
125+
{
126+
accessToken: newAccessToken,
127+
username: user.username,
128+
},
129+
);
130+
});
131+
132+
router.post('/logout', (_req, res) => {
133+
res.clearCookie('refreshToken');
134+
res.status(204).end();
135+
});
136+
137+
export default router;

backend/src/routes/login.ts

-53
Original file line numberDiff line numberDiff line change
@@ -1,53 +0,0 @@
1-
import jwt from 'jsonwebtoken';
2-
import bcrypt from 'bcrypt';
3-
import express from 'express';
4-
import fieldParsers from '../utils/field-parsers';
5-
import logger from '../utils/logger';
6-
import { User } from '../mongo';
7-
8-
const router = express.Router();
9-
10-
router.post('/', async (req, res) => {
11-
const { body } = req;
12-
let loginFields: {
13-
username: string,
14-
password: string,
15-
};
16-
17-
try {
18-
loginFields = fieldParsers.proofLogInFields(body);
19-
} catch (error) {
20-
return res.status(400).send({ error: logger.getErrorMessage(error) });
21-
}
22-
23-
try {
24-
// TODO: populate fields? frontend needs the image and posts of the logged in user.
25-
const user = await User.findOne({ username: loginFields.username });
26-
const isUserValidated = !user
27-
? false
28-
: await bcrypt.compare(loginFields.password, user.passwordHash);
29-
30-
if (!isUserValidated) {
31-
return res.status(401).send({
32-
error: 'Invalid username or password.',
33-
});
34-
}
35-
36-
const userTokenInfo = {
37-
username: user.username,
38-
id: user.id,
39-
};
40-
41-
const token = jwt.sign(
42-
userTokenInfo,
43-
process.env.SECRET = 'scrt',
44-
{ expiresIn: 60 * 60 },
45-
);
46-
47-
return res.status(200).send({ token, username: user.username });
48-
} catch (error) {
49-
return res.status(500).send({ error: 'Something went wrong!' });
50-
}
51-
});
52-
53-
export default router;

backend/src/utils/config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
CLOUDINARY_NAME,
77
CLOUDINARY_API_KEY,
88
CLOUDINARY_API_SECRET,
9+
JWT_SECRET,
910
} = process.env;
1011

1112
export default {
@@ -14,4 +15,5 @@ export default {
1415
CLOUDINARY_NAME,
1516
CLOUDINARY_API_KEY,
1617
CLOUDINARY_API_SECRET,
18+
JWT_SECRET,
1719
};

0 commit comments

Comments
 (0)