Skip to content

Commit

Permalink
Merge pull request #9 from Mintbean/feat/jimmy-test-cookies
Browse files Browse the repository at this point in the history
Feat/jimmy test cookies
  • Loading branch information
jpqy committed Sep 15, 2020
2 parents ec2da3f + aa55b7b commit f90faef
Show file tree
Hide file tree
Showing 18 changed files with 504 additions and 304 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,17 @@
"@types/express": "4.17.7",
"@types/graphql-type-uuid": "0.2.3",
"@types/node": "14.6.0",
"@types/set-cookie-parser": "0.0.6",
"@types/supertest": "2.0.10",
"apollo-server-testing": "2.17.0",
"apollo-server-types": "0.5.1",
"concurrently": "5.3.0",
"cross-env": "7.0.2",
"graphql-type-uuid": "0.2.0",
"jest": "26.4.2",
"nodemon": "2.0.4",
"set-cookie-parser": "2.4.6",
"supertest": "4.0.2",
"ts-jest": "26.3.0",
"ts-node": "9.0.0",
"typescript": "4.0.2"
Expand Down
4 changes: 2 additions & 2 deletions src/buildServer.ts → src/buildApolloServer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ApolloServer } from "apollo-server-express";
import { GraphQLSchema } from "graphql";
import { BuildExpressServerContext } from "./buildContext";
import { BuildExpressServerContext } from "./buildServerContext";

export default function buildServer(schema: GraphQLSchema, context: BuildExpressServerContext): ApolloServer {
export default function buildApolloServer(schema: GraphQLSchema, context: BuildExpressServerContext): ApolloServer {
const apolloServer = new ApolloServer({
schema,
context,
Expand Down
46 changes: 0 additions & 46 deletions src/buildContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import UserService from "./service/UserService";
import UserDaoKnex from "./dao/UserDaoKnex";
import UserResolverValidator from "./validator/UserResolverValidator";
import UserDao from "./dao/UserDao";
import { Request, Response } from "express";
import { clearCookie, setCookie } from "./util/cookieUtils";
import { parseJwt } from "./util/jwtUtils";
import { AuthenticationError } from "apollo-server-express";

export interface PersistenceContext {
userDao: UserDao;
Expand Down Expand Up @@ -37,45 +33,3 @@ export function buildResolverContext(persistenceContext: PersistenceContext): Re
userService,
};
}

export interface ExpressContext {
req: Request;
res: Response;
}

export interface ServerContext {
setCookie: (token: string) => void;
clearCookie: () => void;
userId?: string;
// TODO: include userId and maybe auth scope, which will be parsed from req cookie
}

export type BuildExpressServerContext = (expressContext: ExpressContext) => ServerContext;

export const buildExpressServerContext: BuildExpressServerContext = function ({
req,
res,
}: {
req: Request;
res: Response;
}) {
let userId;
const jwt: string = req.cookies.jwt;

if (jwt) {
try {
const parsedToken = parseJwt(jwt);
userId = parsedToken.sub;
} catch (e) {
// parseJwt throws an error in case of signature mismatch or jwt is expired
res.clearCookie("jwt"); // We need to do this otherwise it will be an infinite loop
throw new AuthenticationError(e.message);
}
}

return {
userId,
setCookie: setCookie(res),
clearCookie: clearCookie(res),
};
};
10 changes: 10 additions & 0 deletions src/buildExpressServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApolloServer } from "apollo-server-express";
import express, { Application } from "express";
import cookieParser from "cookie-parser";

export default function buildExpressServer(apolloServer: ApolloServer): Application {
const app = express();
app.use(cookieParser());
apolloServer.applyMiddleware({ app });
return app;
}
47 changes: 47 additions & 0 deletions src/buildServerContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AuthenticationError } from "apollo-server-express";
import { Request, Response } from "express";
import { setCookie, clearCookie } from "./util/cookieUtils";
import { parseJwt } from "./util/jwtUtils";

export interface ExpressContext {
req: Request;
res: Response;
}

export interface ServerContext {
getUserId(): string;
setJwt(token: string): void;
clearJwt(): void;
// TODO: include userId and maybe auth scope, which will be parsed from req cookie
}

export type BuildExpressServerContext = (expressContext: ExpressContext) => ServerContext;

export const buildExpressServerContext: BuildExpressServerContext = function ({
req,
res,
}: {
req: Request;
res: Response;
}): ServerContext {
// TODO: Clean up later
let userId: string;
const jwt: string = req.cookies.jwt;

if (jwt) {
try {
const parsedToken = parseJwt(jwt);
userId = parsedToken.sub;
} catch (e) {
// parseJwt throws an error in case of signature mismatch or jwt is expired
res.clearCookie("jwt"); // We need to do this otherwise it will be an infinite loop
throw new AuthenticationError(e.message);
}
}

return {
getUserId: () => userId,
setJwt: setCookie(res),
clearJwt: clearCookie(res),
};
};
51 changes: 39 additions & 12 deletions src/graphql/resolver/userResolver.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,59 @@
import { Resolvers } from "../../types/gqlGeneratedTypes";
import { Resolvers, User } from "../../types/gqlGeneratedTypes";
import UserService from "../../service/UserService";
import UserResolverValidator from "../../validator/UserResolverValidator";
import { ServerContext } from "../../buildContext";
import { ServerContext } from "../../buildServerContext";
import { AuthenticationError } from "apollo-server-express";
import { JWTPayload, generateJwt } from "../../util/jwtUtils";

const userResolver = (userResolverValidator: UserResolverValidator, userService: UserService): Resolvers => {
return {
Query: {
user: (_root, args, context: ServerContext) => {
return userResolverValidator.getOne(args, context).then((args) => userService.getOne(args, context));
user: (_root, args, context: ServerContext): Promise<User> => {
return userResolverValidator.getOne(args, context).then((args) => userService.getOne(args));
},

users: (_root, args, context: ServerContext) => {
users: (_root, args, context: ServerContext): Promise<User[]> => {
// TODO: Add validation once we need to validate params that are used for pagination / sorting etc.
return userService.getMany(args, context);
return userService.getMany(args);
},

me: (_root, _args, context: ServerContext) => {
return userService.me(context);
me: (_root, _args, context: ServerContext): Promise<User> => {
const userId = context.getUserId();
if (!userId) {
throw new AuthenticationError("You are not logged in!");
}

return userService.getOne({ id: userId });
},
},

Mutation: {
login: (_root, args, context: ServerContext) => {
return userResolverValidator.login(args, context).then((args) => userService.login(args, context));
login: (_root, args, context: ServerContext): Promise<User> => {
return userResolverValidator.login(args, context).then(async (args) => {
const isValidPassword = await userService.checkPassword(args);
if (!isValidPassword) {
throw new AuthenticationError("Login failed!");
}
// TODO: Move below into jwt auth service
// Make a JWT and return it in the body as well as the cookie
const user = await userService.getOne({ email: args.email });
const payload: JWTPayload = {
sub: user.id,
};
const token = generateJwt(payload);

context.setJwt(token);
return { ...user, token };
});
},

logout: (_root, _args, context: ServerContext) => {
return userService.logout(context);
logout: (_root, _args, context: ServerContext): boolean => {
const userId = context.getUserId();
if (userId) {
context.clearJwt();
return true;
}
return false;
},
},
};
Expand Down
17 changes: 7 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import express from "express";
import { buildPersistenceContext, buildResolverContext, buildExpressServerContext } from "./buildContext";
import { buildPersistenceContext, buildResolverContext } from "./buildContext";
import { buildExpressServerContext } from "./buildServerContext";
import buildSchema from "./buildSchema";
import buildServer from "./buildServer";
import cookieParser from "cookie-parser";
import buildApolloServer from "./buildApolloServer";
import buildExpressServer from "./buildExpressServer";

const persistenceContext = buildPersistenceContext();
const resolverContext = buildResolverContext(persistenceContext);
const schema = buildSchema(resolverContext);
const server = buildServer(schema, buildExpressServerContext);
const app = express();
app.use(cookieParser());
const apolloServer = buildApolloServer(schema, buildExpressServerContext);
const app = buildExpressServer(apolloServer);

server.applyMiddleware({ app });

app.listen({ port: 4000 }, () => console.log(`Server ready at http://localhost:4000${server.graphqlPath}`));
app.listen({ port: 4000 }, () => console.log(`Server ready at http://localhost:4000${apolloServer.graphqlPath}`));
39 changes: 5 additions & 34 deletions src/service/UserService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { User } from "../types/gqlGeneratedTypes";
import { EntityService } from "./EntityService";
import bcrypt from "bcryptjs";
import { AuthenticationError } from "apollo-server-express";
import UserDao from "../dao/UserDao";
import { ServerContext } from "../buildContext";
import { generateJwt, JWTPayload } from "../util/jwtUtils";

export interface UserServiceGetOneArgs {
id?: string | null;
Expand All @@ -25,46 +22,20 @@ export interface UserServiceLoginArgs {
export default class UserService implements EntityService<User> {
constructor(private userDao: UserDao) {}

async getOne(args: UserServiceGetOneArgs, context: ServerContext): Promise<User> {
async getOne(args: UserServiceGetOneArgs): Promise<User> {
return this.userDao.getOne(args);
}

async getMany(args: UserServiceGetManyArgs, context: ServerContext): Promise<User[]> {
async getMany(args: UserServiceGetManyArgs): Promise<User[]> {
return this.userDao.getMany(args);
}

async login(args: UserServiceLoginArgs, context: ServerContext): Promise<User> {
async checkPassword(args: UserServiceLoginArgs): Promise<boolean> {
const user: User = await this.userDao.getOne({ email: args.email });
const correctPassword = await bcrypt.compare(args.password, user.passwordHash);
if (!correctPassword) {
throw new AuthenticationError("Login failed!");
return false;
}

// Make a JWT and return it in the body as well as the cookie
const payload: JWTPayload = {
sub: user.id,
};
const token = generateJwt(payload);

context.setCookie(token);
return { ...user, token };
}

async me(context: ServerContext): Promise<User> {
const { userId } = context;
if (!userId) {
throw new AuthenticationError("You are not logged in!");
}

return this.userDao.getOne({ id: userId });
}

async logout(context: ServerContext): Promise<boolean> {
const { userId } = context;
if (userId) {
context.clearCookie();
return true;
}
return false;
return true;
}
}
2 changes: 1 addition & 1 deletion src/util/cookieUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const cookieOptions: CookieOptions = {
httpOnly: true,
sameSite: "strict", // May need to change if frontend & backend are hosted on different servers
};

// TODO: Put in decorator
export const setCookie = (res: Response) => (token: string): void => {
res.cookie("jwt", token, cookieOptions);
};
Expand Down
2 changes: 1 addition & 1 deletion src/validator/UserResolverValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { QueryUserArgs, User, MutationLoginArgs } from "../types/gqlGeneratedTyp
import { ensureExists } from "../util/ensureExists";
import { UserServiceGetOneArgs, UserServiceLoginArgs } from "../service/UserService";
import UserDao from "../dao/UserDao";
import { ServerContext } from "../buildContext";
import { ServerContext } from "../buildServerContext";

export default class UserResolverValidator {
constructor(private userDao: UserDao) {}
Expand Down

0 comments on commit f90faef

Please sign in to comment.