Skip to content
Merged
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
5 changes: 5 additions & 0 deletions requests/users.rest
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ Content-Type: application/json

###

GET http://127.0.0.1:8080/api/v1/auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjQyMDZmYTEwLWM2NTQtNDRlNi04ZjRhLTU4MWI2NmI0NzE2ZiIsImlzQWRtaW4iOmZhbHNlLCJpYXQiOjE3NDc3NjA3NTgsImV4cCI6MTc0ODAxOTk1OH0.DF7lFdLIqy4MrkvkAWYjn6uOQq8dCcCS5-DibV-aBPA

###

GET http://127.0.0.1:8080/api/v1/users/3f457861-0abb-40a9-9603-dcaad46f224c/posts

###
Expand Down
4 changes: 4 additions & 0 deletions src/api/v1/auth/auth.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ authRouter.post('/signin', async (req, res, next) => {
)(req, res, next);
});

authRouter.get('/me', authValidator, (req, res) => {
res.json(req.user);
});

authRouter.get('/verify', authValidator, (req, res) => {
res.json(true);
});
Expand Down
20 changes: 10 additions & 10 deletions src/api/v1/posts/posts.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
getVoteFilterOptionsFromReqQuery,
} from '../../../lib/helpers';
import { Request, Response, Router } from 'express';
import { AppJwtPayload } from '../../../types';
import { PublicUser } from '../../../types';
import postsService from './posts.service';
import postSchema, { commentSchema } from './post.schema';

Expand Down Expand Up @@ -55,7 +55,7 @@ postsRouter.get('/', optionalAuthValidator, async (req, res) => {
});

postsRouter.get('/count', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const postsCount = await postsService.countPostsByAuthorId(user.id);
res.json(postsCount);
});
Expand All @@ -65,22 +65,22 @@ postsRouter.get('/categories', async (req, res) => {
});

postsRouter.get('/categories/count', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const categoriesCount =
await postsService.countPostsCategoriesByPostsAuthorId(user.id);
res.json(categoriesCount);
});

postsRouter.get('/comments/count', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const commentsCount = await postsService.countPostsCommentsByPostsAuthorId(
user.id
);
res.json(commentsCount);
});

postsRouter.get('/votes/count', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const votesCount = await postsService.countPostsVotesByPostsAuthorId(user.id);
res.json(votesCount);
});
Expand Down Expand Up @@ -148,20 +148,20 @@ postsRouter.get(
);

postsRouter.post('/', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const postData = { ...postSchema.parse(req.body), authorId: user.id };
const createdPost = await postsService.createPost(postData);
res.status(201).json(createdPost);
});

postsRouter.post('/:id/upvote', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const upvotedPost = await postsService.upvotePost(req.params.id, user.id);
res.json(upvotedPost);
});

postsRouter.post('/:id/downvote', authValidator, async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const downvotedPost = await postsService.downvotePost(req.params.id, user.id);
res.json(downvotedPost);
});
Expand All @@ -170,7 +170,7 @@ postsRouter.post(
'/:id/comments',
authValidator,
async (req: Request<{ id: string }, unknown, { content: string }>, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const commentData = commentSchema.parse(req.body);
const updatedPost = await postsService.findPostByIdAndCreateComment(
req.params.id,
Expand All @@ -197,7 +197,7 @@ postsRouter.put(
authValidator,
createOwnerValidator(getCommentAuthorId),
async (req, res) => {
const user = req.user as AppJwtPayload;
const user = req.user as PublicUser;
const commentData = commentSchema.parse(req.body);
const updatedPost = await postsService.findPostCommentByCompoundIdAndUpdate(
req.params.pId,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ if (globalForPrisma.prisma) {
prismaClient = new PrismaClient({
// Read the URL programmatically to support replacing .env with .env.test in CLI
datasourceUrl: process.env.DATABASE_URL,
omit: { user: { password: true, isAdmin: true } },
// Globally omit the password field; need to be sat to false explicitly, to retrieve a user with password
omit: { user: { password: true } },
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import db from './db';
import ms from 'ms';

export const createJwtForUser = (user: PublicUser): string => {
const { id, username, fullname } = user;
const payload: AppJwtPayload = { id, username, fullname };
const { id, isAdmin } = user;
const payload: AppJwtPayload = { id, isAdmin };
const token = jwt.sign(payload, SECRET, {
expiresIn: TOKEN_EXP_PERIOD as ms.StringValue,
});
Expand Down Expand Up @@ -88,7 +88,7 @@ export const getVoteTypeFilterFromReqQuery = (req: Request) => {
export const getSignedInUserIdFromReqQuery = (req: Request) => {
let userId;
if (req.user) {
userId = (req.user as AppJwtPayload).id;
userId = (req.user as PublicUser).id;
}
return userId;
};
Expand Down
11 changes: 10 additions & 1 deletion src/lib/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,16 @@ passport.use(
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: SECRET,
},
(jwtPayload: AppJwtPayload, done) => done(null, jwtPayload)
(jwtPayload: AppJwtPayload, done) => {
const { id } = jwtPayload;
db.user
.findUnique({ where: { id } })
.then((user) => {
if (user) done(null, user);
else done(null, false);
})
.catch((error: unknown) => done(error, false));
}
)
);

Expand Down
23 changes: 7 additions & 16 deletions src/middlewares/validators.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { Request, Response, NextFunction } from 'express';
import { RequestHandler } from 'express';
import { AppJwtPayload } from '../types';
import { PublicUser } from '../types';
import passport from '../lib/passport';
import db from '../lib/db';

const isAdmin = async (id: string): Promise<boolean> => {
const dbUser = await db.user.findUnique({
where: { id },
select: { isAdmin: true },
});
return Boolean(dbUser?.isAdmin);
};

export const createOwnerValidator = (
getOwnerId: (req: Request, res: Response) => unknown
): RequestHandler => {
return async (req: Request, res: Response, next: NextFunction) => {
const reqUser = req.user as AppJwtPayload | undefined;
const reqUser = req.user as PublicUser | undefined;
const owner = reqUser?.id === (await getOwnerId(req, res));
if (owner) next();
else res.status(401).end();
Expand All @@ -27,21 +18,21 @@ export const createAdminOrOwnerValidator = (
getOwnerId: (req: Request, res: Response) => unknown
): RequestHandler => {
return async (req: Request, res: Response, next: NextFunction) => {
const reqUser = req.user as AppJwtPayload | undefined;
const admin = reqUser && (await isAdmin(reqUser.id));
const reqUser = req.user as PublicUser | undefined;
const admin = reqUser?.isAdmin;
const owner = reqUser?.id === (await getOwnerId(req, res));
if (admin || owner) next();
else res.status(401).end();
};
};

export const adminValidator = async (
export const adminValidator = (
req: Request,
res: Response,
next: NextFunction
) => {
const reqUser = req.user as AppJwtPayload | undefined;
const admin = reqUser && (await isAdmin(reqUser.id));
const reqUser = req.user as PublicUser | undefined;
const admin = reqUser?.isAdmin;
if (admin) next();
else res.status(401).end();
};
Expand Down
61 changes: 51 additions & 10 deletions src/tests/api/v1/auth.int.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { AppErrorResponse, AppJwtPayload, AuthResponse } from '../../../types';
import { AppErrorResponse, AuthResponse } from '../../../types';
import { it, expect, describe, afterAll, beforeAll, vi } from 'vitest';
import { User } from '../../../../prisma/generated/client';
import { SIGNIN_URL, VERIFY_URL } from './utils';
import { SIGNIN_URL, VERIFY_URL, SIGNED_IN_USER_URL } from './utils';
import jwt from 'jsonwebtoken';
import setup from '../setup';

describe('Authentication endpoint', async () => {
const { api, userData, createUser, deleteAllUsers } = await setup(SIGNIN_URL);
const {
api,
userData,
createUser,
deleteAllUsers,
prepForAuthorizedTest,
assertUnauthorizedErrorRes,
} = await setup(SIGNIN_URL);

let dbUser: User;

beforeAll(async () => {
await deleteAllUsers();
await createUser(userData);
dbUser = await createUser(userData);
});

afterAll(deleteAllUsers);
Expand Down Expand Up @@ -48,19 +57,21 @@ describe('Authentication endpoint', async () => {
const resUser = resBody.user as User;
const resJwtPayload = jwt.decode(
resBody.token.replace(/^Bearer /, '')
) as AppJwtPayload;
) as User;
expect(res.type).toMatch(/json/);
expect(res.statusCode).toBe(200);
expect(resUser.username).toBe(userData.username);
expect(resUser.fullname).toBe(userData.fullname);
expect(resUser.isAdmin).toStrictEqual(false);
expect(resUser.password).toBeUndefined();
expect(resUser.isAdmin).toBeUndefined();
expect(resBody.token).toMatch(/^Bearer /i);
expect(resJwtPayload.id).toBeTypeOf('string');
expect(resJwtPayload.username).toBe(userData.username);
expect(resJwtPayload.fullname).toBe(userData.fullname);
expect(resJwtPayload.id).toStrictEqual(dbUser.id);
expect(resJwtPayload.isAdmin).toStrictEqual(false);
expect(resJwtPayload.username).toBeUndefined();
expect(resJwtPayload.fullname).toBeUndefined();
expect(resJwtPayload.password).toBeUndefined();
expect(resJwtPayload.isAdmin).toBeUndefined();
expect(resJwtPayload.createdAt).toBeUndefined();
expect(resJwtPayload.updatedAt).toBeUndefined();
});
});

Expand Down Expand Up @@ -98,4 +109,34 @@ describe('Authentication endpoint', async () => {
expect(res.statusCode).toBe(401);
});
});

describe(`GET ${SIGNED_IN_USER_URL}`, () => {
it('should respond with 401 if the user is not found', async () => {
const { authorizedApi } = await prepForAuthorizedTest(userData);
await deleteAllUsers();
const res = await authorizedApi.get(SIGNED_IN_USER_URL);
dbUser = await createUser(userData); // All tests expects this user to be exist
assertUnauthorizedErrorRes(res);
});

it('should respond with 401 if the JWT is invalid', async () => {
const res = await api
.get(SIGNED_IN_USER_URL)
.set('Authorization', 'blah');
assertUnauthorizedErrorRes(res);
});

it('should respond with current signed in user data base on the JWT', async () => {
const { authorizedApi } = await prepForAuthorizedTest(userData);
const res = await authorizedApi.get(SIGNED_IN_USER_URL);
const resBody = res.body as User;
expect(res.statusCode).toBe(200);
expect(res.type).toMatch(/json/);
expect(resBody.password).toBeUndefined();
expect(resBody.id).toStrictEqual(dbUser.id);
expect(resBody.isAdmin).toStrictEqual(false);
expect(resBody.username).toBe(dbUser.username);
expect(resBody.fullname).toBe(dbUser.fullname);
});
});
});
Loading