I am unable to set cookies in my application: Apollo TypeGraphql Express

95 Views Asked by At

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?

0

There are 0 best solutions below