parent
477787785e
commit
586efd5581
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,39 @@ |
|||||||
{ |
{ |
||||||
"name": "pcbizr", |
"name": "pcbizr", |
||||||
"version": "0.0.1", |
"version": "0.0.1", |
||||||
"description": "", |
"description": "", |
||||||
"main": "src/app.js", |
"main": "src/app.js", |
||||||
"scripts": { |
"scripts": { |
||||||
"dev": "ts-node-dev --respawn src/app.ts", |
"dev": "ts-node-dev --respawn src/app.ts", |
||||||
"test": "jest" |
"test": "jest --watch", |
||||||
}, |
"gen:key": "ssh-keygen -t rsa -b 2048 -f src/utils/keys/jwtRS256.key && openssl rsa -in src/utils/keys/jwtRS256.key -pubout -outform PEM -out src/utils/keys/jwtRS256.key.pub" |
||||||
"keywords": [], |
}, |
||||||
"author": "", |
"keywords": [], |
||||||
"license": "ISC", |
"author": "", |
||||||
"dependencies": { |
"license": "ISC", |
||||||
"apollo-server": "^2.6.2", |
"dependencies": { |
||||||
"graphql": "^14.3.1", |
"apollo-server": "^2.9.3", |
||||||
"reflect-metadata": "^0.1.13", |
"argon2": "^0.24.1", |
||||||
"type-graphql": "^0.17.4", |
"dotenv": "^8.1.0", |
||||||
"typeorm": "^0.2.18" |
"graphql": "^14.5.4", |
||||||
}, |
"jsonwebtoken": "^8.5.1", |
||||||
"devDependencies": { |
"pg": "^7.12.1", |
||||||
"@types/faker": "^4.1.5", |
"reflect-metadata": "^0.1.13", |
||||||
"@types/graphql": "^14.2.0", |
"type-graphql": "^0.17.5", |
||||||
"@types/jest": "^24.0.13", |
"typeorm": "^0.2.18" |
||||||
"@types/node": "^12.0.12", |
}, |
||||||
"class-transformer": "^0.2.3", |
"devDependencies": { |
||||||
"faker": "^4.1.0", |
"@types/faker": "^4.1.5", |
||||||
"jest": "^24.8.0", |
"@types/graphql": "^14.5.0", |
||||||
"sqlite3": "^4.0.9", |
"@types/jest": "^24.0.18", |
||||||
"ts-jest": "^24.0.2", |
"@types/js-cookie": "^2.2.2", |
||||||
"ts-node-dev": "^1.0.0-pre.39", |
"@types/jsonwebtoken": "^8.3.3", |
||||||
"typescript": "^3.5.1" |
"@types/node": "^12.7.5", |
||||||
} |
"class-transformer": "^0.2.3", |
||||||
} |
"faker": "^4.1.0", |
||||||
|
"jest": "^24.9.0", |
||||||
|
"ts-jest": "^24.1.0", |
||||||
|
"ts-node-dev": "^1.0.0-pre.42", |
||||||
|
"typescript": "^3.6.3" |
||||||
|
} |
||||||
|
} |
||||||
|
@ -1,14 +1,23 @@ |
|||||||
import "reflect-metadata"; |
import "reflect-metadata" |
||||||
import { Field, ObjectType } from "type-graphql"; |
import { Field, ObjectType } from "type-graphql" |
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; |
import { BaseEntity, BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm" |
||||||
|
import * as argon2 from "../utils/argon2" |
||||||
|
|
||||||
@ObjectType() |
@ObjectType() |
||||||
@Entity() |
@Entity() |
||||||
export class User extends BaseEntity { |
export class User extends BaseEntity { |
||||||
@PrimaryGeneratedColumn() |
@PrimaryGeneratedColumn() |
||||||
id!: number
|
id!: number |
||||||
|
|
||||||
@Field() |
@Field() |
||||||
@Column() |
@Column() |
||||||
email: string = "" |
email: string = "" |
||||||
|
|
||||||
|
@Column() |
||||||
|
password: string = "" |
||||||
|
|
||||||
|
@BeforeInsert() |
||||||
|
async hashPassword() { |
||||||
|
this.password = await argon2.hashIncludingOptions(this.password) |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -1,59 +1,90 @@ |
|||||||
import faker from "faker"; |
|
||||||
import { createConnection, getConnection } from "typeorm"; |
import faker = require("faker") |
||||||
import { callSchema } from "../../utils/callSchema"; |
import { createConnection, getConnection } from "typeorm" |
||||||
import { User } from "../User"; |
import { callSchema } from "../../utils/callSchema" |
||||||
|
import * as jwt from "../../utils/jwt" |
||||||
const usersQuery = ` |
import { User } from "../User" |
||||||
query { |
|
||||||
users { |
beforeAll(async () => { |
||||||
email |
return await createConnection({ |
||||||
} |
type: "postgres", |
||||||
}` |
host: "localhost", |
||||||
|
port: 5432, |
||||||
beforeEach(() => { |
database: "testing", |
||||||
return createConnection({ |
username: "postgres", |
||||||
type: "sqlite", |
password: "postgres", |
||||||
database: ":memory:", |
// dropSchema: true,
|
||||||
dropSchema: true, |
|
||||||
entities: [User], |
entities: [User], |
||||||
synchronize: true, |
synchronize: true, |
||||||
logging: false |
logging: false, |
||||||
}); |
}) |
||||||
}); |
}) |
||||||
|
|
||||||
afterEach(() => { |
afterAll(async () => { |
||||||
let conn = getConnection(); |
return await getConnection().close() |
||||||
return conn.close(); |
}) |
||||||
}); |
|
||||||
|
afterEach(async () => { |
||||||
describe("resolver of", () => { |
return await getConnection().synchronize(true) |
||||||
describe("users query", () => { |
}) |
||||||
it("should return an empty array when no users are created", async () => { |
|
||||||
const response = await callSchema({ |
describe("resolver of user", () => { |
||||||
source: usersQuery, |
it("returns email as it creates user with mutation", async () => { |
||||||
}) |
|
||||||
|
const fakeEmail = faker.internet.email() |
||||||
expect(response).toMatchObject({ |
const fakePassword = faker.internet.password(6) |
||||||
data: { |
const createUserMutation = `mutation {
|
||||||
users: [], |
createUser(email: "${fakeEmail}", password: "${fakePassword}") { |
||||||
}, |
email |
||||||
}) |
} |
||||||
|
}` |
||||||
|
|
||||||
|
const response = await callSchema(createUserMutation) |
||||||
|
|
||||||
|
expect(response).toMatchObject({ |
||||||
|
data: { |
||||||
|
createUser: { email: fakeEmail }, |
||||||
|
}, |
||||||
}) |
}) |
||||||
|
}) |
||||||
|
|
||||||
|
it("should return emails of registered users", async () => { |
||||||
|
|
||||||
|
const usersQuery = `{
|
||||||
|
users { |
||||||
|
email |
||||||
|
} |
||||||
|
}` |
||||||
|
|
||||||
it("should return a populated array when an user is created", async () => { |
const user = await User.create({ |
||||||
const user = await User.create({ |
email: faker.internet.email(), |
||||||
email: faker.internet.email(), |
}).save() |
||||||
}).save() |
|
||||||
|
|
||||||
const response = await callSchema({ |
const response = await callSchema(usersQuery) |
||||||
source: usersQuery, |
|
||||||
}) |
|
||||||
|
|
||||||
expect(response).toMatchObject({ |
expect(response).toMatchObject({ |
||||||
data: { |
data: { |
||||||
users: [{ email: user.email }], |
users: [{ email: user.email }], |
||||||
}, |
}, |
||||||
}) |
|
||||||
}) |
}) |
||||||
}) |
}) |
||||||
|
|
||||||
|
it("should 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() |
||||||
|
}) |
||||||
}) |
}) |
||||||
|
@ -1,14 +1,45 @@ |
|||||||
import "reflect-metadata"; |
import "reflect-metadata" |
||||||
import { Query, Resolver } from "type-graphql"; |
import { Arg, Mutation, Query, Resolver } from "type-graphql" |
||||||
import { getRepository } from "typeorm"; |
import * as argon2 from "../../utils/argon2" |
||||||
import { User } from "../User"; |
import * as jwt from "../../utils/jwt" |
||||||
|
import { User } from "../User" |
||||||
|
|
||||||
@Resolver(_of => User) |
@Resolver(() => User) |
||||||
export class UserResolver { |
export class UserResolver { |
||||||
|
@Query(() => [User]) |
||||||
@Query(_returns => [User]) |
|
||||||
async users() { |
async users() { |
||||||
const userRepository = getRepository(User) |
return await User.find() |
||||||
return userRepository.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 |
||||||
|
} |
||||||
|
|
||||||
|
@Mutation(() => User) |
||||||
|
async createUser( |
||||||
|
@Arg("email") email: string, |
||||||
|
@Arg("password") password: string |
||||||
|
): Promise<User> { |
||||||
|
return await User.create({ |
||||||
|
email, |
||||||
|
password, |
||||||
|
}).save() |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,10 @@ |
|||||||
|
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,7 +1,16 @@ |
|||||||
import { buildSchema } from "type-graphql"; |
import { buildSchema, MiddlewareFn } from "type-graphql" |
||||||
import { UserResolver } from "../modules/User/UserResolver"; |
import { UserResolver } from "../modules/User/UserResolver" |
||||||
|
|
||||||
|
const ErrorInterceptor: MiddlewareFn<any> = async ({}, next) => { |
||||||
|
try { |
||||||
|
return await next() |
||||||
|
} catch (err) { |
||||||
|
console.error(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
export const createSchema = () => |
export const createSchema = () => |
||||||
buildSchema({ |
buildSchema({ |
||||||
resolvers: [UserResolver], |
resolvers: [UserResolver], |
||||||
|
globalMiddlewares: [ErrorInterceptor], |
||||||
}) |
}) |
||||||
|
@ -0,0 +1,12 @@ |
|||||||
|
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