I've included all of the files in reference to the setting cookies and the process. I am new to a graphql express app so maybe there are some obvious things I am not doing but for the love of me I cannot find out what it is and this is supposed to be the easy part.
But I cannot seem to set any cookies.
package.json
{
"name": "typegraphql-tutorial",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts"
},
"author": "AJ Shannon",
"license": "ISC",
"dependencies": {
"@typegoose/typegoose": "^11.0.0",
"apollo-server": "^3.12.0",
"bcrypt": "^5.1.0",
"class-validator": "^0.14.0",
"config": "^3.3.9",
"cookie-parser": "^1.4.6",
"dotenv": "^16.0.3",
"graphql": "^15.8.0",
"jsonwebtoken": "^9.0.0",
"mongoose": "^7.0.3",
"nanoid": "^4.0.2",
"reflect-metadata": "^0.1.13",
"type-graphql": "^1.1.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/config": "^0.0.40",
"@types/cookie-parser": "^1.4.2",
"@types/jsonwebtoken": "^8.5.6",
"ts-node-dev": "^1.1.8",
"typescript": "^4.5.2"
}
}
user.resolver.ts
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
import { CreateUserInput, LoginInput, User } from "../schema/user.schema";
import UserService from "../services/user.service";
import Context from "../types/context";
@Resolver()
export default class UserResolver {
constructor(private userService: UserService) {
this.userService = new UserService();
}
@Mutation(() => User)
createUser(@Arg("input") input: CreateUserInput) {
return this.userService.createUser(input);
}
@Mutation(() => String) // Returns the JWT
login(@Arg("input") input: LoginInput, @Ctx() context: Context) {
return this.userService.login(input, context);
}
@Query(() => User, { nullable: true })
me(@Ctx() context: Context) {
return context.user;
}
}
user.schema.ts
import { Field, InputType, ObjectType } from "type-graphql";
import { IsEmail, IsNotEmpty, MaxLength, MinLength } from "class-validator";
import {
ReturnModelType,
getModelForClass,
index,
pre,
prop,
queryMethod,
} from "@typegoose/typegoose";
import { AsQueryMethod } from "@typegoose/typegoose/lib/types";
import bcrypt from "bcrypt";
function findByEmail(this: ReturnModelType<typeof User, QueryHelpers>, email: User['email']) {
return this.findOne({email})
}
interface QueryHelpers {
findByEmail: AsQueryMethod<typeof findByEmail>
}
@pre<User>('save', async function() {
// Check if the password is being modified
if(!this.isModified('password')) {
return
}
const salt = await bcrypt.genSalt(10);
const hash = await bcrypt.hashSync(this.password, salt);
this.password = hash;
})
@index({email: 1})
@queryMethod(findByEmail)
@ObjectType()
export class User {
@Field(() => String)
_id: string;
@Field(() => String)
@prop({required: true})
name: string;
@Field(() => String)
@prop({required: true})
email: string;
@Field(() => String)
@prop({required: true})
password: string;
}
export const UserModel = getModelForClass<typeof User, QueryHelpers>(User);
@InputType()
export class CreateUserInput {
@Field(() => String)
name: string;
@IsEmail()
@Field(() => String)
email: string;
@MinLength(6, {
message: 'password must be at least 6 characters long'
})
@MaxLength(50, {
message: 'password must be no longer than 50 characters long'
})
@Field(() => String)
password: string;
}
@InputType()
export class LoginInput {
@Field(() => String)
@IsNotEmpty()
email: string;
@Field(() => String)
@IsNotEmpty()
password: string;
}
user.service.ts
import { ApolloError } from "apollo-server-errors";
import bcrypt from "bcrypt";
import { CreateUserInput, LoginInput, UserModel } from "../schema/user.schema";
import Context from "../types/context";
import { signJwt } from "../utils/jwt";
class UserService {
async createUser(input: CreateUserInput) {
return UserModel.create(input);
}
async login(input: LoginInput, context: Context) {
const e = "Invalid email or password";
// Get our user by email
const user = await UserModel.find().findByEmail(input.email).lean();
if (!user) {
throw new ApolloError(e);
}
// validate the password
const passwordIsValid = await bcrypt.compare(input.password, user.password);
if (!passwordIsValid) {
throw new ApolloError(e);
}
// sign a jwt
const token = signJwt(user);
// set a cookie for the jwt
context.res.cookie("accessToken", token, {
maxAge: 3.154e10, // 1 year
httpOnly: true,
domain: "localhost",
path: "/",
sameSite: "none",
secure: true,
// sameSite: "strict",
// secure: process.env.NODE_ENV === "production",
});
console.log(context.req.cookies.accessToken);
console.log(context.req.cookies)
context.res.setHeader('x-forwarded-proto', 'https');
// return the jwt
return token;
}
}
export default UserService;
authChecker.ts
import { AuthChecker } from "type-graphql";
import Context from "../types/context";
import { AuthenticationError } from "apollo-server";
const authChecker: AuthChecker<Context> = ({ context }) => {
if (!context.user) {
throw new AuthenticationError("You must be authenticated to access this resource");
}
return true;
};
export default authChecker;
jwt.ts
import jwt from "jsonwebtoken";
const publicKey = Buffer.from(
config.get<string>("publicKey"),
"base64"
).toString("ascii");
const privateKey = Buffer.from(
config.get<string>("privateKey"),
"base64"
).toString("ascii");
const signOptions: jwt.SignOptions = {
algorithm: 'RS256',
};
export function signJwt(object: object, options?: jwt.SignOptions): string {
try {
return jwt.sign(object, privateKey, {
...(options || {}),
...signOptions,
});
} catch (error) {
console.error('Failed to sign JWT:', error);
throw new Error('Failed to sign JWT');
}
}
export function verifyJwt<T>(token: string): T | null {
try {
const decoded = jwt.verify(token, publicKey) as T;
return decoded;
} catch (e) {
return null;
}
}
index.ts
import dotenv from 'dotenv';
dotenv.config();
import 'reflect-metadata';
import { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageProductionDefault } from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { connectToMongo } from './utils/mongo';
import cookieParser from "cookie-parser";
import authChecker from './utils/authChecker';
import express from 'express';
import { resolvers } from './resolvers';
import { verifyJwt } from './utils/jwt';
import { User } from './schema/user.schema';
import Context from './types/context';
import { AuthenticationError, ApolloError } from 'apollo-server';
async function bootstrap(){
// Build a schema
const schema = await buildSchema({
resolvers,
authChecker
})
// Init express
const app = express();
app.set('trust proxy', process.env.NODE_ENV !== 'production')
app.use(cookieParser());
// Create the apollo server
const server = new ApolloServer({
schema,
context: (ctx: Context) => {
const context = ctx;
if (ctx.req.cookies.accessToken) {
try {
const user = verifyJwt<User>(ctx.req.cookies.accessToken);
context.user = user;
console.log(user);
} catch (error) {
console.error('Failed to verify JWT:', error);
throw new AuthenticationError('Invalid access token');
}
}
return context;
},
plugins: [
process.env.NODE_ENV === "production"
? ApolloServerPluginLandingPageProductionDefault()
: ApolloServerPluginLandingPageGraphQLPlayground(),
],
formatError: (error) => {
if (error.originalError && error.originalError instanceof AuthenticationError) {
// This is an authentication error
return new ApolloError("You must be authenticated to access this resource", "UNAUTHENTICATED");
}
// For all other errors, return the original error object
return error;
},
});
// Server.start()
await server.start()
// Apply Middleware
server.applyMiddleware({app})
// App.listen on express server
app.listen ({ port: 4000}, () => {
console.log("App is listening on http://localhost:4000");
})
// Connect to db
connectToMongo();
}
bootstrap();
Now in the graphql playground I can login and receive a token.
mutation login($input: LoginInput!){ login(input: $input) }
but I only get null when requesting the user.
query { me { _id name email } }
Ive tried console logging the cookies array, and the user once its set but I am getting no feedback or new errors.
Can anyone please take a look?