What I would like to do is:
create login mutation and return using it user id and username
add a cookie to a browser (chrome) with user id to check if user is logged in or not
Login mutation works if I do not assign userId to session, works means it returns requested data.
However when I add below line of code to add userId to session, it creates a cookie but I can't fetch data (userId and userName):
req.session!.userId = user.id;
I receive information i can't fetch data, please check your connection.
I am not sure when I am making mistake, any help appreciated.
Below I provide my setup:
index.ts
import "reflect-metadata"
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { buildSchema } from 'type-graphql';
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/post";
import { UserResolver } from "./resolvers/user";
import session from 'express-session';
import IORedis from "ioredis";
import { MyContext } from "./types";
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
const main = async () => {
//console.log("dirname: ", __dirname);
// connect db
const orm = await MikroORM.init(microConfig);
// run migrations
await orm.getMigrator().up();
const app = express();
// importing connect redis causing issue with session object
const RedisStore = require('connect-redis')(session)
//const RedisStore = connectRedis(session);
//const redisClient = redis.createClient();
const redisClient: IORedis.Cluster = new IORedis.Cluster([])
// https://github.com/tj/connect-redis/issues/300
// installed ioredis
app.use(
session({
name: "rjsession",
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
httpOnly: true,
sameSite: 'lax',
//secure: __prod__, //cookie works only in https, on dev good to run off
secure: false,
},
saveUninitialized: false,
secret: 'keyboard catadasdasdsdaasdadas',
resave: false,
})
)
const apolloServer = new ApolloServer({
schema: await buildSchema( {
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }): MyContext => ({ em: orm.em, req, res }),
// added plugin to force apollo server to use old graphql playground
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground({
// options
})
],
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: { credentials: true, origin: ["http://localhost:4000/"] },
});
app.listen(4000, () => {
console.log("server started on localhost: 4000")
})
app.get('/', (_, res) => {
res.send("hello");
})
};
// catch error and print it
main().catch(err => {
console.log(err);
});
login mutation definition:
#Mutation(() => UserResponse)
async login(
#Arg('options') options: UsernamePasswordInput,
#Ctx() { em, req }: MyContext
): Promise<UserResponse> {
// check if user registered
const user = await em.findOne(User, { username: options.username });
// if user null return password
if (!user) {
return {
errors: [
{
field: "username",
message: "User does not exist.",
},
],
};
}
// validate password against user input
const valid = await argon2.verify(user.password, options.password);
// return error if password failed validation
if (!valid){
return {
errors: [
{
field: "password",
message: "password incorrect.",
},
],
};
}
// asign user id in cookies
req.session!.userId = user.id;
console.log(req.session!.userId)
//req.session!.userId = user.id;
// if no errors return user
return {
user,
};
}
types.ts file:
import { EntityManager, IDatabaseDriver, Connection } from "#mikro-orm/core";
import { Response, Request} from 'express'
//import { Request } from 'express-serve-static-core'
export type MyContext = {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>
//req: Request & {session: Express.Session} & { userId: number };
//req: Request & {session: Session};
req: Request; // & {session: Express.Session};
res: Response;
}
Based on your current code, you are using req.session!.userId only at one place, and you use you are getting error. That makes me think that you have an issue with redis.
When you login, your server is going to generate a session id which is stored on the Redis store. Since your redis is not running on your machine, you cannot store it so you are getting error.
Before you start your application, start the redis server first.
Related
I'm following Fullstack React GraphQL TypeScript Tutorial by Ben Awad, but I encountered considerable difficulty during approximately (1:55:00) set up cookies.
I think I connect to Redis successfully, and set express-session, req type, but I'm unable to see my cookies in chrome->applications->cookies.
I try to follow this post, but it's not working-ish for me.
index.ts
const app = express();
app.set("trust proxy", true);
app.set("Access-Control-Allow-Origin", "https://studio.apollographql.com");
app.set("Access-Control-Allow-Credentials", true);
const RedisStore = connectRedis(session)
const redisClient = createClient({ legacyMode: true })
redisClient.connect().catch(console.error)
app.use(
session({
name: 'qid',
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 year
httpOnly: true,
secure: true, // cookies only works on https if it's true // set to __prod__ by ben awad
sameSite: "none" // csrf
},
saveUninitialized: false,
secret: "izodfg",
resave: false,
})
)
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }): MyContext => ({ em: emFork, req, res })
})
const cors = { // add for apollo studio
credentials: true,
origin: 'https://studio.apollographql.com'
}
await apolloServer.start();
apolloServer.applyMiddleware({ app, cors });
type.ts
export type MyContext = {
em: EntityManager<IDatabaseDriver<Connection>>
req: Request & { session: { userId?: string } }
res: Response
};
./resolvers/user.ts
#Mutation(() => UserResponse)
async login(
#Arg('options') options: UsernamePasswordInput,
#Ctx() { em, req }: MyContext
): Promise<UserResponse> {
const user = await em.findOne(User, { username: options.username });
if (!user) {
return {
errors: [
{
field: "username",
message: "that username doesn't exists",
}
]
}
}
const vaild = await argon2.verify(user.password, options.password);
if (!vaild) {
return {
errors: [
{
field: "password",
message: "incorrect password",
}
]
}
}
req.session.userId = user.id.toString();
return {
user
}
}
Redis Inspect
stored cookie in Redis picture
I have a small app that allows registration and login, but I'm still trying to use session-express to persist the session.
Below is server.ts where I create the session, cors, etc...
import express, { json } from "express";
import { db } from "./models/db-connection";
import { router } from "./routes";
import session from "express-session";
var cors = require("cors");
const app = express();
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "GET"],
credentials: true,
})
);
app.use(json());
app.use(
session({
secret: "testtest",
resave: false,
saveUninitialized: false,
})
);
app.use(router);
app.listen(3001, async () => {
try {
await db.sync();
console.log("Connected to the database");
} catch (error) {
console.error("Failed to connect to the database", error);
}
});
In the routes.ts script I use the authenticate function which will only allow a new user to be registered if an user is already logged in.
But the problem is exactly here, req.session.authenticated is never true, it is always undefined, even when I set it to true as I will show in UserController.ts.
Below is routes.ts.
import express from "express";
import UserController from "./controllers/UserController";
import "./session-data";
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.redirect("/login");
}
}
const router = express.Router();
router.post("/users", authenticate, UserController.create);
router.get("/users/login/:login", UserController.findLogin);
export { router };
As you can see below in UserController.ts, req.session.authenticated is true when we find a match, I put in a console.log just to confirm that req.session has the authenticated property at this point, and it does, but it looks like routes.ts can't see it.
UserController.ts
import express, { Request, Response } from "express";
import { UserModel } from "../models/UserModel";
import "../session-data";
const bcrypt = require("bcryptjs");
class UserController {
async findLogin(req: express.Request, res: express.Response) {
const email = req.query.email?.toString();
const password = req.query.password?.toString();
try {
const user: any = await UserModel.findOne({
where: {
email: email,
},
});
if (user) {
const match = await bcrypt.compare(password, user.password);
if (match) {
req.session.authenticated = true;
console.log(req.session);
return res.status(204).json(user);
} else {
req.session.authenticated = false;
return res.status(200).send("invalid password");
}
} else {
req.session.authenticated = false;
return res.status(201).send("User not found");
}
} catch (error: any) {
req.session.authenticated = false;
return res.send(error.message);
}
}
}
async create(req: Request, res: Response) {
try {
const { userName, email, password } = req.body;
const user = await UserModel.create({
userName,
email,
password,
});
return res.status(201).json(user);
} catch (error: any) {
console.error(error);
return res.send(error.message);
}
}
Since I'm using Typescript, I need to create a session-data.ts file to expand req.session
session-data.ts
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
export {};
In the session store, the session is never created either.
Could you help me please? I don't know why req.session.authenticated isn't working, I'm new to using typescript, I imagine there's something related to that.
Thanks!
Try simplifying your code as much as possible so that the problem is still reproducible. The following works for me:
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
function login(req: express.Request, res: express.Response, next: express.NextFunction) {
req.session.authenticated = true;
res.end("Logged in");
}
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.end("Not logged in");
}
}
express()
.use(session({
secret: "Se$$ion",
resave: false,
saveUninitialized: false
}))
.get("/login", login)
.get("/auth", authenticate, function(req, res) {
res.end("Welcome");
})
.listen(3001);
GET /login returns "Logged in".
Then GET /auth return "Welcome" and the session is logged:
Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
authenticated: true
}
I am using Next.js and have successfully added passport.js, thus getting a user back after a successful login. \0/
However what I want is the req.user to be available to my api/user route...
In this example repo in middleware/auth.js
It mentions in a use.() function below...
Initialize mocked database Remove this after you add your own
database
import nextConnect from 'next-connect'
import passport from '../lib/passport'
import session from '../lib/session'
const auth = nextConnect()
.use(
session({
name: 'sess',
secret: process.env.TOKEN_SECRET,
cookie: {
maxAge: 60 * 60 * 8, // 8 hours,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
},
})
)
.use((req, res, next) => {
// Initialize mocked database
// Remove this after you add your own database
req.session.users = req.session.users || []
next()
})
.use(passport.initialize())
.use(passport.session())
export default auth
This is my api/user route:
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
const handler = nextConnect()
handler
.use(auth)
.get((req, res) => {
// You do not generally want to return the whole user object
// because it may contain sensitive field such as !!password!! Only return what needed
// const { _id } = req.user
// res.json({ user: { _id } })
console.log("in api/user req.user ", req.user); // returns undefined.
if (req.user) {
const { _id } = req.user
res.json({ user: { _id } })
} else {
res.json({ user: null })
}
})
export default handler;
My rational is the repo also has a hook which calls the /api/user route to see if the req.user exists...
import useSWR from 'swr'
export const fetcher = (url) => fetch(url).then((r) => r.json())
export function useUser() {
const { data, mutate } = useSWR('/api/user', fetcher)
// if data is not defined, the query has not completed
console.log("data in useUser hooks!", data);
console.log("data?.user ", data?.user);
const loading = !data
const user = data?.user
return [user, { mutate, loading }]
}
By the contents of the repo it would appear you should be able to get req.user , from setting up the database in the middleware/auth file and how would you do this and thus get it in the api/user route?
Instead of using req.user use req.session and then console.log this
I'm following along with Ben Awad's 13-hour Fullstack React GraphQL TypeScript Tutorial and encountered a wall during the login cookie setting (aprx at 1:50:00).
I think I successfully connected to redis, set express-session and set req type but in graphql sandbox I don't see my cookie (named 'qid') at Inspect->Application.
index.ts
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/post";
import { UserResolver } from "./resolvers/user";
import redis from "redis";
import session from "express-session";
import connectRedis from "connect-redis";
const main = async () => {
const orm = await MikroORM.init(microConfig);
await orm.getMigrator().up();
const app = express();
const RedisStore = connectRedis(session);
const redisClient = redis.createClient();
app.use(
session({
name: "qid",
store: new RedisStore({
client: redisClient,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
sameSite: "none",
// secure: __prod__,
},
saveUninitialized: false,
secret: "dfhfdjkgfkbjktzkzf",
resave: false,
})
);
app.use(function (req, res, next) {
res.header(
"Access-Control-Allow-Origin",
"https://studio.apollographql.com"
);
res.header("Access-Control-Allow-Credentials", "true");
next();
});
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }) => ({ em: orm.em, req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: {
credentials: true,
origin: new RegExp("/*/"),
},
});
app.listen(4000, () => {
console.log("server started on port 4000");
});
};
main();
types.ts
import { EntityManager, IDatabaseDriver, Connection } from "#mikro-orm/core";
import { Request, Response } from "express";
import { Session, SessionData } from "express-session";
export type MyContext = {
em: EntityManager<any> & EntityManager<IDatabaseDriver<Connection>>;
req: Request & {
session: Session & Partial<SessionData> & { userId: number };
};
res: Response;
};
and my userResolver (user.ts)
import { User } from "../entities/User";
import { MyContext } from "../types";
import {
Arg,
Ctx,
Field,
InputType,
Mutation,
ObjectType,
Query,
Resolver,
} from "type-graphql";
import argon2 from "argon2";
#InputType()
class UsernamePasswordInput {
#Field()
username: string;
#Field()
password: string;
}
#ObjectType()
class FieldError {
#Field()
field: string;
#Field()
message: string;
}
#ObjectType()
class UserResponse {
#Field(() => [FieldError], { nullable: true })
errors?: FieldError[];
#Field(() => User, { nullable: true })
user?: User;
}
#Resolver()
export class UserResolver {
#Mutation(() => UserResponse)
async login(
#Arg("options", () => UsernamePasswordInput) options: UsernamePasswordInput,
#Ctx() { em, req }: MyContext
): Promise<UserResponse> {
const user = await em.findOne(User, { username: options.username });
if (!user) {
return {
errors: [
{
field: "username",
message: "username does not exist",
},
],
};
}
const valid = await argon2.verify(user.password, options.password);
if (!valid) {
return {
errors: [
{
field: "password",
message: "incorrect password",
},
],
};
}
req.session.userId = user.id;
return {
user,
};
}
}
I tried setting up res.headers as graphql sandbox is asking but still to no avail. Would appreciate any help, thank you!
Okay I'm not sure what is happening, but I seemingly solved the issue.
My idea is that: GraphQL Playground is retired and localhost:port/graphql now redirects to Apollo GraphQL Sandbox to a different url and my guess is that the cookies do not get transfered to this location but the cookie is set at localhost.
So there is a way you can force Apollo to still use the Playground by adding:
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";
const apolloServer = new ApolloServer({
...,
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground({
// options
}),
],
});
And this way Playground shows up and you can set
"request.credentials": "include",
in the settings and voila the cookie shows up at localhost:port.
I hope this helps anyone with this issue - however I'm still not exactly sure that this is a right solution.
Adding the old playground as a plugin probably works but, since they say it is being deprecated, if you want to make it work with the new Apollo Studio, here is how I managed to do it:
I added these three lines right after initializing the app:
app.set("trust proxy", !process.env.NODE_ENV === "production");
app.set("Access-Control-Allow-Origin", "https://studio.apollographql.com");
app.set("Access-Control-Allow-Credentials", true);
Here is how the configuration of my session looks like:
const RedisStore = connectRedis(session);
const redisClient = redis.createClient();
app.use(
session({
saveUninitialized: false,
store: new RedisStore({ client: redisClient }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 1, // 1 year
httpOnly: true,
sameSite: "none",
secure: true, // if true, studio works, postman doesn't; if false its the other way around
},
name: "qid",
secret: "keyboard cat",
resave: false,
}),
);
Then, over to Apollo Studio, go to Connection Settings -> Edit -> Include Cookies (this one was really hard to find):
Make sure to send this header with every request to login: x-forwarded-proto: https
spent some time on this one. Try this combined solution:
import { MikroORM } from "#mikro-orm/core";
import { __prod__ } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { PostResolver } from "./resolvers/Post";
import { UserResolver } from "./resolvers/User";
import session from "express-session";
import connectRedis from "connect-redis";
import { createClient } from "redis";
const main = async () => {
try {
const orm = await MikroORM.init(microConfig);
orm.getMigrator().up();
const app = express();
app.set("trust proxy", process.env.NODE_ENV !== "production"); //a little fix here from another users codes--- actually think this made it works
app.set("Access-Control-Allow-Origin", "https://studio.apollographql.com");
app.set("Access-Control-Allow-Credentials", true);
let redisClient = createClient({ legacyMode: true });
redisClient.connect().catch(console.error);
let RedisStore = connectRedis(session);
const cors = {
credentials: true,
origin: "https://studio.apollographql.com",
};
app.use(
session({
name: "qid",
store: new RedisStore({ client: redisClient as any, disableTTL: true }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
secure: true,
sameSite: "none",
},
saveUninitialized: false,
secret: "keyboardcaxt",
resave: false,
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }) => ({ em: orm.em, req, res }),
});
await apolloServer.start();
apolloServer.applyMiddleware({ app, cors });
app.listen(4000, () => {
console.log("EXPRESS SERVER IS RUNNINGG");
});
} catch (error) {
console.log(error, "ERRR");
}
};
main();
Also dont forget to this
setup and hard-reset your https://studio.apollographql.com/sandbox/.
And this: add ENV to your root
Then you should be ready to go.
I am having trouble with sessions. For some reason, the req.session is undefined even though I'm using the
session middleware. I was trying to use redis but I couldn't make the connection work.
The strange part is that for some reason the cookie is registered in the graphql playground. So the reason must be in the way I pass the request, probably.
All the types are correct ( typescript isn't angry with anything ).
Here's the code from the server.ts
import express, { Request, Response } from "express";
import routes from "./routes";
import cors from "cors";
import "reflect-metadata";
import { createConnection } from "typeorm";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import session from "express-session";
createConnection()
.then(async (connection) => {
console.log("Conexão feita com sucesso");
const app = express();
app.set("trust proxy", 1);
app.use(cors());
app.use(
session({
name: "qid",
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: 1000 * 60 * 60 * 24 * 365 * 10,
httpOnly: true,
},
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [
],
validate: false, // Activate the validation with class-validation module.
}),
context: (req: Request, res: Response): Context => ({ req, res, session: req.session }),
playground: {
settings: {
'request.credentials': 'include',
},
},
});
apolloServer.applyMiddleware({ app });
app.use(express.json());
app.use(routes);
app.listen(3333);
})
.catch((error) => console.error(error));
And where I use the session.
#Mutation(() => UserResponse)
async login(
#Arg("info", () => UserLoginInputType) info: UserLoginInputType,
#Ctx(){req, res, session}: Context
): Promise<UserResponse> {
const user = await User.findOneOrFail({ where: { email: info.email } });
const valid = await argon2.verify(user.password, info.password);
if (valid) {
req.session.userId = user.id;
return {
user,
};
}
return {
errors: [
{
field: "password",
message: "Incorrect password",
},
],
};
}
I just forgot the curly brackets when passing res and req throw the context
you need to pass context something like this and you are good to go
context: ({ req, res }): Context => ({
req,
res,
session: req.session,
}),
also, it's better to do the configuration in the GraphQL config file to include the credentials.
graphql.config.ts
import { ApolloDriverConfig, ApolloDriver } from '#nestjs/apollo';
import { join } from 'path';
export const GraphQLConfig: ApolloDriverConfig = {
driver: ApolloDriver,
debug: true,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
playground: {
settings: {
'editor.theme': 'light', // use value dark if you want a dark theme in the playground
'request.credentials': 'include',
},
},
};
and assign the config file to the module directory
user.module.ts
import { Module } from '#nestjs/common';
import { GraphQLModule } from '#nestjs/graphql';
import { TypeOrmModule } from '#nestjs/typeorm';
import { GraphQLConfig } from 'src/config/graphql.config';
import { UserEntity } from 'src/entity/user.entity';
import { UserResolver } from './user.resolver';
import { UserService } from './user.service';
#Module({
imports: [
TypeOrmModule.forFeature([UserEntity]),
GraphQLModule.forRoot(GraphQLConfig),
],
providers: [UserService, UserResolver],
})
export class UserModule {}
and it will automatically enable credentials value to be "include" from "omit"