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
Related
I'm following a tutorial for Ben Awad and i'm trying to set a userId in an express session after the user logs in to save the cookies in the browser. But when i check the req.session.userId its always undefined.
this is my index.ts
`
const RedisStore = connectRedis(session);
const redis = new Redis({
port: 6379,
host: "127.0.0.1",
});
redis.on("connect", () => console.log("Redis redisClient Connected"));
redis.on("error", (err) => console.log("Redis Client Connection Error", err));
app.use(
session({
name: "SessionID",
store: new RedisStore({
client: redis,
disableTouch: true,
}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, //10 years
httpOnly: true,
sameSite: "lax",
domain: _prod_ ? ".codeponder.com" : undefined,
secure: _prod_, // cookie only works in https
},
secret: "adsadafasdasdasfasfdasda",
resave: false,
saveUninitialized: true,
})
);
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver],
validate: false,
}),
context: ({ req, res }): MyContext => ({ em: orm.em, res, req, redis }),
});
await apolloServer.start();
apolloServer.applyMiddleware({
app,
cors: {
credentials: true,
origin: ["http://localhost:3000", "https://studio.apollographql.com"],
},
});
app.listen(4000, () => {
console.log("server started on localhost:4000");
});
};
`
and when i try to save the userId in the login. Here when i log the req.session after assigning it to the user.id i can see it working
`
#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 doesnt exist",
},
],
};
}
const valid = await argon2.verify(user.password, options.password);
if (!valid) {
return {
errors: [
{
field: "password",
message: "incorrect password",
},
],
};
}
req.session.userId = user.id;
console.log("req.>>", req.session);
req.session.save(() => {
console.log("saved");
});
return { user };
}
}
`
But when i check if the user is logged in or not from this function. req.session.userId is always undefined and not saved in the session
`
#Query(() => User, { nullable: true })
async me(#Ctx() { req, em }: MyContext) {
console.log("req", req.session);
if (!req.session.userId) {
return null;
}
const user = await em.findOne(User, { id: req.session.userId });
return user;
}
`
i tried upgrading/downgrading the packages. Checked my redis connection. Nothing seem to work
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.
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.
Platform Used:
NestJs 7.1.2
Postgres
After a certain duration my aws rds session count keeps increasing and the server becomes unresponsive. I need to restart the server again to make it working.
My database connection code in main.ts:
import * as session from 'express-session';
app.use(
session({
store: new (require('connect-pg-simple')(session))({
conString:
'pg://' +
process.env.TYPEORM_USERNAME +
':' +
process.env.TYPEORM_PASSWORD +
'#' +
process.env.TYPEORM_HOST +
'/' +
process.env.TYPEORM_DATABASE,
}),
secret: process.env.COOKIE_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 }, // 30 days
}),
);
app.module.ts code:
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService],
})
where my database config is as follows:
import * as path from 'path';
export default {
type: process.env.TYPEORM_CONNECTION,
host: process.env.TYPEORM_HOST,
port: +process.env.TYPEORM_PORT,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
synchronize: true,
dropSchema: false,
logging: false,
retryAttempts: 5,
keepConnectionAlive: true,
entities: [path.join(__dirname, '../') + '**/!(*.d).entity{.ts,.js}'],
migrations: ['migrations/**/*.ts'],
subscribers: ['subscriber/**/*.ts', 'dist/subscriber/**/.js'],
cli: {
entitiesDir: 'src',
migrationsDir: 'migrations',
subscribersDir: 'subscriber',
},
};
Kindly assist...have struggled a lot on this!!
Use the pgPromise connection param instead of the conString param. Also modify apolloServer+express-session configuration settings.
async function main() {
const connection = await createConnection(ormconfig as ConnectionOptions);
const schema = await buildSchema({ resolvers: [DysfunctionalThoughtResolver,UserResolver] });
const server = new ApolloServer({
schema,
context: async ({ req, res }) => ({ req, res }),
playground: {
settings: {
"request.credentials": "same-origin",
}
}
});
var app=express();
var pgSession = require('connect-pg-simple')(session);
app.use(session({
name: process.env.COOKIE_NAME,
saveUninitialized:false,
secret:process.env.COOKIE_SECRET?.split(','),
cookie: {
store: new pgSession({
pgPromise : connection
}),
httpOnly: true,
secure: process.env.NODE_ENV=="prod",
sameSite: true,
maxAge: 600000 // Time is in miliseconds
},
resave: false
}))
await app.listen({port:4000});
server.applyMiddleware({ app });
console.log("Server has started!");
}
main();
Dont forget to add the session table required in connect-pg-simple:
https://github.com/voxpelli/node-connect-pg-simple/blob/HEAD/table.sql
CREATE TABLE "session" (
"sid" varchar NOT NULL COLLATE "default",
"sess" json NOT NULL,
"expire" timestamp(6) NOT NULL
)
WITH (OIDS=FALSE);
ALTER TABLE "session" ADD CONSTRAINT "session_pkey" PRIMARY KEY ("sid") NOT DEFERRABLE INITIALLY IMMEDIATE;
CREATE INDEX "IDX_session_expire" ON "session" ("expire");
TypeORM translation(put in a session.ts file in your models/entities folder).
#Entity()
export class session {
#PrimaryColumn({
type: "varchar",
nullable: false,
//collation:"" //needed?
})
sid:string;
#Column({
type: "json",
nullable: false
})
sess:any;
#Index("IDX_session_expire")
#Column({
type: "timestamp",
precision: 6,
nullable: false
})
expire:Date;
}
Then access it in your resolvers
#Mutation(() => String)
async login(#Args() { email, password }: UserLoginArgs, #Ctx() ctx: any) {
let response = "";
let sess = ctx.req.session;
if (sess.userId) { response = "already logged in"; }
else {
let user = await User.findOne({ where: { email: email } });
if (user) {
let passwordsMatch = await bcrypt.compare(password, user?.password);
if (passwordsMatch) {
sess.userId = user.id;
sess.save();
response = "login successful";
} else {
response = "Password doesn't match"
}
} else {
response = "User not found";
}
}
return response;
}
#Mutation(() => String)
async logout(#Ctx() ctx: any) {
let sess = ctx.req.session;
if (!sess.userId) { return "already logged out"; }
else{
sess.destroy();
return "logged out";
}
}
I want to get user property in session after login, but when i set session after login, socket.request.session doesn't change, there is no user property in it. But i can get it in router.post.
I tried to edit sessions in router.post()/router.get(), socket.request.session was still not change.
index.js
const io = require('socket.io').listen('8082', { origins: '*' })
const schedule = require('node-schedule')
io.set('transports', [
'websocket',
'flashsocket',
'htmlfile',
'xhr-polling',
'jsonp-polling',
'polling'
])
io.set('origins', '*:*')
const app = new express()
const sessionStore = new session.MemoryStore({ reapInterval: 3600 * 1000 })
const sessionMiddleware = session({
secret: 'Stefanie Sun',
store: sessionStore,
resave: false,
saveUninitialized: true,
cookie: {
maxAge: 8 * 3600 * 1000,
secure: false
},
rolling: true
})
io.use((socket, next) => {
sessionMiddleware(socket.request, socket.request.res, next);
})
app.use(sessionMiddleware)
routes(app)
io.on('connection', (socket) => {
const comment_schedule = schedule.scheduleJob('*/10 * * * * *', async () => { /** 10s查询一次是否有新的内容 **/
const COOKIE_STR = socket.request.session
console.log(COOKIE_STR)
})
})
router.js
...
router.post('/pagelist', (req, res, next) => {
req.session.user = { username: 'test1', gender: 'x', bio: 'this is a test' }
console.log(req.session)
res.status(200).json({ code: 'OK' })
}
app.user('/api/page', router)
when i visited /api/page/pagelist after signin, i wish i can get the same session in socket.request and req.session in router.post,
but socket.request.session is
Session {
cookie:
{ path: '/',
_expires: 2019-04-07T22:57:13.379Z,
originalMaxAge: 28800000,
httpOnly: true,
secure: false } }
but in router.post req.session is
Session {
cookie:
{ path: '/',
_expires: 2019-04-07T22:57:48.146Z,
originalMaxAge: 28799999,
httpOnly: true,
secure: false },
user:
{
username: 'test1',
gender: 'x',
bio: 'this is a test'
}}
i hope in socket.request.session i can also get
user property.
My fontend project is with Vue.js. And i set socket.on('event') in home.vue. I connect socket.io before the page logins, and after login i set user property in session, i can't get this property in socket.request.session.
login code in frontend
// api.js
export const loginApi = ({ username, password } = { username: '', password: '' }) => {
const data = {
username,
password
};
return axios.request({
url: '/api/signin',
data: qs.stringify(data),
method: 'post'
})
}
// login.vue
submit() {
loginApi({ username: this.username, password: this.password})
.then((result: any) => {
this.$toast.success('login success');
this.Cookies.set('user', this.username, { expires: 7 });
getUserInfo(this.username).then((res: any) => {
const { data } = res;
localStorage.setItem('user', JSON.stringify(data));
const url = this.redirect && this.redirect || '/pages';
this.$router.push(url);
})
})
}