parent
1d1e5230d8
commit
d4a6faad5a
@ -0,0 +1,150 @@ |
|||||||
|
import { gql } from "apollo-server" |
||||||
|
import { createConnection, getConnection } from "typeorm" |
||||||
|
import { signToken, verifyToken } from "../auth" |
||||||
|
import { callSchema } from "../schema" |
||||||
|
import { User } from "../User" |
||||||
|
|
||||||
|
beforeAll(async () => { |
||||||
|
return await createConnection({ |
||||||
|
type: "postgres", |
||||||
|
host: "localhost", |
||||||
|
port: 5432, |
||||||
|
database: "testing", |
||||||
|
username: "postgres", |
||||||
|
password: "postgres", |
||||||
|
// dropSchema: true,
|
||||||
|
entities: [User], |
||||||
|
synchronize: true, |
||||||
|
logging: false, |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
afterAll(async () => { |
||||||
|
return await getConnection().close() |
||||||
|
}) |
||||||
|
|
||||||
|
afterEach(async () => { |
||||||
|
return await getConnection().synchronize(true) |
||||||
|
}) |
||||||
|
|
||||||
|
describe("resolver of user", () => { |
||||||
|
describe("createUser mutation should", () => { |
||||||
|
it("return email as it creates user with mutation", async () => { |
||||||
|
const createUserMutation = gql` |
||||||
|
mutation { |
||||||
|
createUser(email: "email@email.com", password: "password") { |
||||||
|
email |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
const response = await callSchema(createUserMutation) |
||||||
|
|
||||||
|
expect(response.errors).toBeUndefined() |
||||||
|
expect(response.data).toMatchObject({ |
||||||
|
createUser: { email: "email@email.com" }, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe("users query should", () => { |
||||||
|
it("return emails of registered users", async () => { |
||||||
|
const usersQuery = gql` |
||||||
|
query { |
||||||
|
users { |
||||||
|
email |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
const user = await User.create({ |
||||||
|
email: "email@email.com", |
||||||
|
}).save() |
||||||
|
|
||||||
|
const response = await callSchema(usersQuery) |
||||||
|
|
||||||
|
expect(response.errors).toBeUndefined() |
||||||
|
expect(response.data).toMatchObject({ |
||||||
|
users: [{ email: user.email }], |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe("loginTokens query should", () => { |
||||||
|
const loginTokensQuery = gql` |
||||||
|
query { |
||||||
|
loginTokens(email: "email@email.com", password: "good-password") { |
||||||
|
accessToken |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
it("return error for non-existent user", async () => { |
||||||
|
const response = await callSchema(loginTokensQuery) |
||||||
|
|
||||||
|
expect(response.errors).not.toBeUndefined() |
||||||
|
expect(response.data).toBeNull() |
||||||
|
}) |
||||||
|
|
||||||
|
it("return error for bad password", async () => { |
||||||
|
await User.create({ |
||||||
|
email: "email@email.com", |
||||||
|
password: "BAD-password", |
||||||
|
}).save() |
||||||
|
|
||||||
|
const response = await callSchema(loginTokensQuery) |
||||||
|
|
||||||
|
expect(response.errors).not.toBeUndefined() |
||||||
|
expect(response.data).toBeNull() |
||||||
|
}) |
||||||
|
|
||||||
|
it("return a valid access token with good credentials", async () => { |
||||||
|
await User.create({ |
||||||
|
email: "email@email.com", |
||||||
|
password: "good-password", |
||||||
|
}).save() |
||||||
|
|
||||||
|
const response = await callSchema(loginTokensQuery) |
||||||
|
const token = response.data!.loginTokens.accessToken |
||||||
|
|
||||||
|
expect(verifyToken(token)).toBeTruthy() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe("me query should", () => { |
||||||
|
const meQuery = gql` |
||||||
|
query { |
||||||
|
me { |
||||||
|
email |
||||||
|
} |
||||||
|
} |
||||||
|
` |
||||||
|
|
||||||
|
it("return an error without a valid jwt token", async () => { |
||||||
|
const response = await callSchema(meQuery) |
||||||
|
|
||||||
|
expect(response.errors).not.toBeUndefined() |
||||||
|
expect(response.data).toBeNull() |
||||||
|
}) |
||||||
|
|
||||||
|
it("return an user with a valid jwt token", async () => { |
||||||
|
const user = await User.create({ |
||||||
|
email: "email@email.com", |
||||||
|
}).save() |
||||||
|
|
||||||
|
const context = { |
||||||
|
req: { |
||||||
|
headers: { |
||||||
|
authorization: "Bearer " + signToken({ userId: user.id }), |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const response = await callSchema(meQuery, context) |
||||||
|
|
||||||
|
expect(response.errors).toBeUndefined() |
||||||
|
expect(response.data).toMatchObject({ |
||||||
|
me: { email: user.email }, |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,62 @@ |
|||||||
|
import "reflect-metadata" |
||||||
|
import { Arg, Authorized, Ctx, Field, Mutation, ObjectType, Query, Resolver } from "type-graphql" |
||||||
|
import { comparePassword, MyContext, signToken } from "../auth" |
||||||
|
import { User } from "../User" |
||||||
|
|
||||||
|
@ObjectType() |
||||||
|
class LoginTokens { |
||||||
|
@Field() |
||||||
|
accessToken: string = "" |
||||||
|
} |
||||||
|
|
||||||
|
@Resolver(() => User) |
||||||
|
export class UserResolver { |
||||||
|
@Query(() => [User]) |
||||||
|
async users() { |
||||||
|
return await User.find() |
||||||
|
} |
||||||
|
|
||||||
|
@Query(() => LoginTokens) |
||||||
|
async loginTokens( |
||||||
|
@Arg("email") email: string, |
||||||
|
@Arg("password") password: string |
||||||
|
): Promise<LoginTokens> { |
||||||
|
const user = await User.findOne({ where: { email } }) |
||||||
|
|
||||||
|
if (!user) { |
||||||
|
throw new Error("could not find user") |
||||||
|
} |
||||||
|
|
||||||
|
const passwordValid = await comparePassword(user.password, password) |
||||||
|
|
||||||
|
if (!passwordValid) { |
||||||
|
throw new Error("password not valid") |
||||||
|
} |
||||||
|
|
||||||
|
const accessToken = signToken({ userId: user.id }) |
||||||
|
|
||||||
|
return { |
||||||
|
accessToken, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Query(() => User) |
||||||
|
@Authorized() |
||||||
|
async me(@Ctx() { payload }: MyContext) { |
||||||
|
const id = payload!.userId |
||||||
|
const user = await User.findOne({ where: { id } }) |
||||||
|
|
||||||
|
return user |
||||||
|
} |
||||||
|
|
||||||
|
@Mutation(() => User) |
||||||
|
async createUser( |
||||||
|
@Arg("email") email: string, |
||||||
|
@Arg("password") password: string |
||||||
|
): Promise<User> { |
||||||
|
return await User.create({ |
||||||
|
email, |
||||||
|
password, |
||||||
|
}).save() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
import { argon2id, hash, verify as argonVerify } from "argon2" |
||||||
|
import { Request, Response } from "express" |
||||||
|
import { readFileSync } from "fs" |
||||||
|
import { sign, verify as jwtVerify } from "jsonwebtoken" |
||||||
|
import { join } from "path" |
||||||
|
import { AuthChecker } from "type-graphql" |
||||||
|
|
||||||
|
export type Payload = { |
||||||
|
userId: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface MyContext { |
||||||
|
req: Request |
||||||
|
res: Response |
||||||
|
payload?: Payload |
||||||
|
} |
||||||
|
|
||||||
|
export async function hashPassword(password: string) { |
||||||
|
return await hash(password, { type: argon2id }) |
||||||
|
} |
||||||
|
|
||||||
|
export async function comparePassword(hash: string, plain: string) { |
||||||
|
return await argonVerify(hash, plain, { type: argon2id }) |
||||||
|
} |
||||||
|
|
||||||
|
export function signToken(payload: Payload) { |
||||||
|
const PRIVATE_KEY = readFileSync(join(__dirname, "auth", "jwtRS256.key")) |
||||||
|
|
||||||
|
return sign(payload, PRIVATE_KEY, { algorithm: "RS256" }) |
||||||
|
} |
||||||
|
|
||||||
|
export function verifyToken(token: string) { |
||||||
|
const PUBLIC_KEY = readFileSync(join(__dirname, "auth", "jwtRS256.key.pub")) |
||||||
|
|
||||||
|
return jwtVerify(token, PUBLIC_KEY) |
||||||
|
} |
||||||
|
|
||||||
|
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => { |
||||||
|
const authHeader = context.req.headers["authorization"] |
||||||
|
|
||||||
|
if (!authHeader) { |
||||||
|
throw new Error("authorization header is missing") |
||||||
|
} |
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1] |
||||||
|
|
||||||
|
if (!token) { |
||||||
|
throw new Error("token not present in authorization header") |
||||||
|
} |
||||||
|
|
||||||
|
const payload = verifyToken(token) |
||||||
|
|
||||||
|
if (!payload) { |
||||||
|
throw new Error("payload not present in the token") |
||||||
|
} |
||||||
|
|
||||||
|
context.payload = payload as any |
||||||
|
return true |
||||||
|
} |
@ -1,105 +0,0 @@ |
|||||||
import faker = require("faker") |
|
||||||
import { createConnection, getConnection } from "typeorm" |
|
||||||
import { callSchema } from "../../utils/callSchema" |
|
||||||
import * as jwt from "../../utils/jwt" |
|
||||||
import { User } from "../User" |
|
||||||
|
|
||||||
beforeAll(async () => { |
|
||||||
return await createConnection({ |
|
||||||
type: "postgres", |
|
||||||
host: "localhost", |
|
||||||
port: 5432, |
|
||||||
database: "testing", |
|
||||||
username: "postgres", |
|
||||||
password: "postgres", |
|
||||||
// dropSchema: true,
|
|
||||||
entities: [User], |
|
||||||
synchronize: true, |
|
||||||
logging: false, |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
afterAll(async () => { |
|
||||||
return await getConnection().close() |
|
||||||
}) |
|
||||||
|
|
||||||
afterEach(async () => { |
|
||||||
return await getConnection().synchronize(true) |
|
||||||
}) |
|
||||||
|
|
||||||
describe("resolver of user", () => { |
|
||||||
describe("createUser mutation should", () => { |
|
||||||
it("return email as it creates user with mutation", async () => { |
|
||||||
const fakeEmail = faker.internet.email() |
|
||||||
const fakePassword = faker.internet.password(8) |
|
||||||
const createUserMutation = `mutation {
|
|
||||||
createUser(email: "${fakeEmail}", password: "${fakePassword}") { |
|
||||||
email |
|
||||||
} |
|
||||||
}` |
|
||||||
|
|
||||||
const response = await callSchema(createUserMutation) |
|
||||||
|
|
||||||
expect(response.errors).toBeUndefined() |
|
||||||
expect(response.data).toMatchObject({ |
|
||||||
createUser: { email: fakeEmail }, |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe("users query should", () => { |
|
||||||
it("return emails of registered users", async () => { |
|
||||||
const usersQuery = `{
|
|
||||||
users { |
|
||||||
email |
|
||||||
} |
|
||||||
}` |
|
||||||
|
|
||||||
const user = await User.create({ |
|
||||||
email: faker.internet.email(), |
|
||||||
}).save() |
|
||||||
|
|
||||||
const response = await callSchema(usersQuery) |
|
||||||
|
|
||||||
expect(response.errors).toBeUndefined() |
|
||||||
expect(response.data).toMatchObject({ |
|
||||||
users: [{ email: user.email }], |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe("loginToken query should", () => { |
|
||||||
it("return a valid login token", async () => { |
|
||||||
const fakeEmail = faker.internet.email() |
|
||||||
const fakePassword = faker.internet.password(6) |
|
||||||
await User.create({ |
|
||||||
email: fakeEmail, |
|
||||||
password: fakePassword, |
|
||||||
}).save() |
|
||||||
|
|
||||||
const loginTokenQuery = `{
|
|
||||||
loginToken(email: "${fakeEmail}", password: "${fakePassword}")
|
|
||||||
}` |
|
||||||
|
|
||||||
const response = await callSchema(loginTokenQuery) |
|
||||||
const token = response.data!.loginToken |
|
||||||
|
|
||||||
expect(jwt.verify(token, jwt.PUBLIC_KEY)).toBeTruthy() |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe("me query should", () => { |
|
||||||
it("return an error and null data without a valid jwt token", async () => { |
|
||||||
const meQuery = `{
|
|
||||||
me { |
|
||||||
email |
|
||||||
} |
|
||||||
}` |
|
||||||
|
|
||||||
const response = await callSchema(meQuery) |
|
||||||
|
|
||||||
expect(response.errors).not.toBeUndefined() |
|
||||||
expect(response.data).toBeNull() |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
@ -1,54 +0,0 @@ |
|||||||
import "reflect-metadata" |
|
||||||
import { Arg, Authorized, Mutation, Query, Resolver } from "type-graphql" |
|
||||||
import * as argon2 from "../../utils/argon2" |
|
||||||
import * as jwt from "../../utils/jwt" |
|
||||||
import { User } from "../User" |
|
||||||
|
|
||||||
@Resolver(() => User) |
|
||||||
export class UserResolver { |
|
||||||
@Query(() => [User]) |
|
||||||
async users() { |
|
||||||
return await User.find() |
|
||||||
} |
|
||||||
|
|
||||||
@Query(() => String, { nullable: true }) |
|
||||||
async loginToken( |
|
||||||
@Arg("email") email: string, |
|
||||||
@Arg("password") password: string |
|
||||||
): Promise<string | null> { |
|
||||||
const user = await User.findOne({ where: { email } }) |
|
||||||
|
|
||||||
if (!user) { |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
const passwordValid = await argon2.verify(user.password, password) |
|
||||||
|
|
||||||
if (!passwordValid) { |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
const token = jwt.signWithRS256({ userId: user.id }) |
|
||||||
return token |
|
||||||
} |
|
||||||
|
|
||||||
@Query(() => User) |
|
||||||
@Authorized() |
|
||||||
async me() { |
|
||||||
const user = User.create({ |
|
||||||
email: "asddsf@fdsfs.sk", |
|
||||||
}) |
|
||||||
return user |
|
||||||
} |
|
||||||
|
|
||||||
@Mutation(() => User) |
|
||||||
async createUser( |
|
||||||
@Arg("email") email: string, |
|
||||||
@Arg("password") password: string |
|
||||||
): Promise<User> { |
|
||||||
return await User.create({ |
|
||||||
email, |
|
||||||
password, |
|
||||||
}).save() |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,24 @@ |
|||||||
|
import { DocumentNode, graphql, GraphQLSchema } from "graphql" |
||||||
|
import { buildSchema } from "type-graphql" |
||||||
|
import { customAuthChecker } from "./auth" |
||||||
|
import { UserResolver } from "./User/UserResolver" |
||||||
|
|
||||||
|
let schema: GraphQLSchema |
||||||
|
|
||||||
|
export const callSchema = async (document: DocumentNode, context?: any) => { |
||||||
|
if (!schema) { |
||||||
|
schema = await createSchema() |
||||||
|
} |
||||||
|
|
||||||
|
return graphql({ |
||||||
|
schema, |
||||||
|
source: document.loc!.source.body || "", |
||||||
|
contextValue: context, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const createSchema = () => |
||||||
|
buildSchema({ |
||||||
|
resolvers: [UserResolver], |
||||||
|
authChecker: customAuthChecker, |
||||||
|
}) |
@ -1,10 +0,0 @@ |
|||||||
import * as agron2 from "argon2" |
|
||||||
export * from "argon2" |
|
||||||
|
|
||||||
/** |
|
||||||
* Override the defaul agron2i option with agron2id |
|
||||||
* @param password Pasword to has using argon2id |
|
||||||
*/ |
|
||||||
export async function hashIncludingOptions(password: string) { |
|
||||||
return await agron2.hash(password, { type: agron2.argon2id }) |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
import { graphql, GraphQLSchema } from "graphql"; |
|
||||||
import { createSchema } from "./createSchema"; |
|
||||||
|
|
||||||
|
|
||||||
let schema: GraphQLSchema |
|
||||||
|
|
||||||
export const callSchema = async ( source : string) => { |
|
||||||
if (!schema) { |
|
||||||
schema = await createSchema() |
|
||||||
} |
|
||||||
return graphql({ |
|
||||||
schema, |
|
||||||
source, |
|
||||||
}) |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
import { AuthChecker, buildSchema, MiddlewareFn } from "type-graphql" |
|
||||||
// import { User } from "../modules/User"
|
|
||||||
import { UserResolver } from "../modules/User/UserResolver" |
|
||||||
|
|
||||||
const errorInterceptor: MiddlewareFn<any> = async ({}, next) => { |
|
||||||
try { |
|
||||||
return await next() |
|
||||||
} catch (err) { |
|
||||||
console.error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const customAuthChecker: AuthChecker<any> = () => false |
|
||||||
// { root, args, context, info },
|
|
||||||
// roles,
|
|
||||||
// ) => {
|
|
||||||
// console.log(`root: `)
|
|
||||||
// console.log(root)
|
|
||||||
// console.log(`args: `)
|
|
||||||
// console.log(args)
|
|
||||||
// console.log(`context: `)
|
|
||||||
// console.log(context)
|
|
||||||
// console.log(`info: `)
|
|
||||||
// console.log(info)
|
|
||||||
// console.log(`roles: `)
|
|
||||||
// console.log(roles)
|
|
||||||
// // here we can read the user from context
|
|
||||||
// // and check his permission in the db against the `roles` argument
|
|
||||||
// // that comes from the `@Authorized` decorator, eg. ["ADMIN", "MODERATOR"]
|
|
||||||
|
|
||||||
// return false; // or false if access is denied
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const createSchema = () => |
|
||||||
buildSchema({ |
|
||||||
resolvers: [UserResolver], |
|
||||||
globalMiddlewares: [errorInterceptor], |
|
||||||
authChecker: customAuthChecker, |
|
||||||
// authMode: "null"
|
|
||||||
}) |
|
@ -1,12 +0,0 @@ |
|||||||
import * as jwt from "jsonwebtoken" |
|
||||||
export * from "jsonwebtoken" |
|
||||||
|
|
||||||
import fs = require('fs') |
|
||||||
import path = require('path') |
|
||||||
|
|
||||||
const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, 'keys', 'jwtRS256.key')) |
|
||||||
export const PUBLIC_KEY = fs.readFileSync(path.join(__dirname, 'keys', 'jwtRS256.key.pub'))
|
|
||||||
|
|
||||||
export function signWithRS256(payload: string | object) { |
|
||||||
return jwt.sign(payload, PRIVATE_KEY, {algorithm: "RS256"}) |
|
||||||
} |
|
Loading…
Reference in new issue