rework the folder structure to fractal pattern

master
Peter Babič 5 years ago
parent d4a6faad5a
commit b3128ede9a
Signed by: peter.babic
GPG Key ID: 4BB075BC1884BA40
  1. 77
      package.json
  2. 62
      src/User/UserResolver.ts
  3. 45
      src/app.ts
  4. 48
      src/app/UserResolver.spec.ts
  5. 54
      src/app/UserResolver.ts
  6. 39
      src/app/schema.ts
  7. 8
      src/app/userResolver/Tokens.ts
  8. 22
      src/app/userResolver/User.ts
  9. 56
      src/app/userResolver/auth.ts
  10. 59
      src/auth.ts
  11. 24
      src/schema.ts

@ -1,40 +1,41 @@
{
"name": "pcbizr",
"version": "0.0.1",
"description": "",
"main": "src/app.js",
"scripts": {
"dev": "ts-node-dev --respawn src/app.ts",
"test": "jest -i --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": "",
"license": "ISC",
"dependencies": {
"apollo-server": "^2.9.3",
"apollo-server-express": "^2.9.4",
"argon2": "^0.24.1",
"dotenv": "^8.1.0",
"express": "^4.17.1",
"graphql": "^14.5.4",
"jsonwebtoken": "^8.5.1",
"pg": "^7.12.1",
"reflect-metadata": "^0.1.13",
"type-graphql": "^0.17.5",
"typeorm": "^0.2.18"
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/graphql": "^14.5.0",
"@types/jest": "^24.0.18",
"@types/js-cookie": "^2.2.2",
"@types/jsonwebtoken": "^8.3.3",
"@types/node": "^12.7.5",
"class-transformer": "^0.2.3",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.42",
"typescript": "^3.6.3"
}
"name": "pcbizr",
"version": "0.0.1",
"description": "",
"main": "src/app.js",
"scripts": {
"dev": "ts-node-dev --respawn src/app.ts",
"test": "jest -i --coverage",
"test:watch": "jest -i --watch",
"gen:key": "ssh-keygen -t rsa -b 2048 -f src/app/userResolver/auth/jwtRS256.key && openssl rsa -in src/app/userResolver/auth/wtRS256.key -pubout -outform PEM -out src/app/userResolver/auth/wtRS256.key.pub"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server": "^2.9.3",
"apollo-server-express": "^2.9.4",
"argon2": "^0.24.1",
"dotenv": "^8.1.0",
"express": "^4.17.1",
"graphql": "^14.5.4",
"jsonwebtoken": "^8.5.1",
"pg": "^7.12.1",
"reflect-metadata": "^0.1.13",
"type-graphql": "^0.17.5",
"typeorm": "^0.2.18"
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/graphql": "^14.5.0",
"@types/jest": "^24.0.18",
"@types/js-cookie": "^2.2.2",
"@types/jsonwebtoken": "^8.3.3",
"@types/node": "^12.7.5",
"class-transformer": "^0.2.3",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.42",
"typescript": "^3.6.3"
}
}

@ -1,62 +0,0 @@
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()
}
}

@ -1,37 +1,26 @@
require("dotenv").config()
import { ApolloServer } from "apollo-server-express"
import { createConnection } from "typeorm"
import { createSchema } from "./schema"
import { User } from "./User"
import { connectionOptions, createSchema } from "./app/schema"
import express = require("express")
;(async () => {
await createConnection({
type: "postgres",
host: "localhost",
port: 5432,
database: "postgres",
username: "postgres",
password: "postgres",
// dropSchema: true,
entities: [User],
synchronize: true,
logging: false,
})
await createConnection(connectionOptions)
const server = new ApolloServer({
schema: await createSchema(),
playground: true,
introspection: true,
debug: true,
context: ({ req, res }) => ({ req, res }),
})
const server = new ApolloServer({
schema: await createSchema(),
playground: true,
introspection: true,
debug: true,
context: ({ req, res }) => ({ req, res }),
})
const app = express()
server.applyMiddleware({ app })
const app = express()
server.applyMiddleware({ app })
const PORT = process.env.PORT || 4000
app.listen({ port: PORT }, () =>
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}. `)
)
const APP_PORT = process.env.APP_PORT || 4000
app.listen({ port: APP_PORT }, () =>
console.log(
`🚀 Server ready at http://localhost:${APP_PORT}${server.graphqlPath}. `
)
)
})()

@ -1,22 +1,12 @@
import { gql } from "apollo-server"
import { createConnection, getConnection } from "typeorm"
import { signToken, verifyToken } from "../auth"
import { callSchema } from "../schema"
import { User } from "../User"
import { callSchema, connectionOptions } from "./schema"
import { signToken, verifyToken } from "./userResolver/auth"
import { Tokens } from "./userResolver/Tokens"
import { User } from "./userResolver/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,
})
return await createConnection(connectionOptions)
})
afterAll(async () => {
@ -70,16 +60,17 @@ describe("resolver of user", () => {
})
})
describe("loginTokens query should", () => {
const loginTokensQuery = gql`
describe("tokens query should", () => {
const tokensQuery = gql`
query {
loginTokens(email: "email@email.com", password: "good-password") {
tokens(email: "email@email.com", password: "good-password") {
accessToken
}
}
`
it("return error for non-existent user", async () => {
const response = await callSchema(loginTokensQuery)
const response = await callSchema(tokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
@ -91,7 +82,7 @@ describe("resolver of user", () => {
password: "BAD-password",
}).save()
const response = await callSchema(loginTokensQuery)
const response = await callSchema(tokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
@ -103,9 +94,13 @@ describe("resolver of user", () => {
password: "good-password",
}).save()
const response = await callSchema(loginTokensQuery)
const token = response.data!.loginTokens.accessToken
const response = await callSchema(tokensQuery)
const token = response.data!.tokens.accessToken
const tokens = new Tokens()
tokens.accessToken = token
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({ tokens })
expect(verifyToken(token)).toBeTruthy()
})
})
@ -120,7 +115,14 @@ describe("resolver of user", () => {
`
it("return an error without a valid jwt token", async () => {
const response = await callSchema(meQuery)
const context = {
req: {
headers: {
authorization: "Bearer INVALID-TOKEN",
},
},
}
const response = await callSchema(meQuery, context)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()

@ -0,0 +1,54 @@
import "reflect-metadata"
import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql"
import { comparePassword, MyContext, signToken } from "./userResolver/auth"
import { Tokens } from "./userResolver/Tokens"
import { User } from "./userResolver/User"
export class UserResolver {
@Query(() => [User])
async users() {
return await User.find()
}
@Query(() => Tokens)
async tokens(
@Arg("email") email: string,
@Arg("password") password: string
): Promise<Tokens> {
try {
const user = await User.findOne({ where: { email } })
if (!(await comparePassword(user!.password, password))) {
throw new Error()
}
const accessToken = signToken({ userId: user!.id })
return {
accessToken,
}
} catch (error) {
throw new Error("login credentials are invalid")
}
}
@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,39 @@
require("dotenv").config()
import { DocumentNode, graphql, GraphQLSchema } from "graphql"
import { buildSchema } from "type-graphql"
import { ConnectionOptions } from "typeorm"
import { UserResolver } from "./UserResolver"
import { customAuthChecker } from "./userResolver/auth"
import { User } from "./userResolver/User"
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,
})
export const connectionOptions: ConnectionOptions = {
type: "postgres",
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASS,
entities: [User],
synchronize: true,
logging: false,
}

@ -0,0 +1,8 @@
import "reflect-metadata"
import { Field, ObjectType } from "type-graphql"
@ObjectType()
export class Tokens {
@Field()
accessToken: string = ""
}

@ -6,18 +6,18 @@ import { hashPassword } from "./auth"
@ObjectType()
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number
@PrimaryGeneratedColumn()
id!: number
@Field()
@Column()
email: string = ""
@Field()
@Column()
email: string = ""
@Column()
password: string = ""
@Column()
password: string = ""
@BeforeInsert()
async hashPassword() {
this.password = await hashPassword(this.password)
}
@BeforeInsert()
async hashPassword() {
this.password = await hashPassword(this.password)
}
}

@ -0,0 +1,56 @@
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"
let PRIVATE_KEY: Buffer
let PUBLIC_KEY: Buffer
export type Payload = {
userId: number
}
export interface MyContext {
req: Request
res: Response
payload?: Payload
}
export const hashPassword = async (password: string) =>
await hash(password, { type: argon2id })
export const comparePassword = async (hash: string, plain: string) =>
await argonVerify(hash, plain, { type: argon2id })
export const signToken = (payload: Payload) => {
if (!PRIVATE_KEY) {
PRIVATE_KEY = readKeyFile("jwtRS256.key")
}
return sign(payload, PRIVATE_KEY, { algorithm: "RS256" })
}
export const verifyToken = (token: string) => {
if (!PUBLIC_KEY) {
PUBLIC_KEY = readKeyFile("jwtRS256.key.pub")
}
return jwtVerify(token, PUBLIC_KEY)
}
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
try {
const authHeader = context.req.headers["authorization"]
const token = authHeader!.split(" ")[1]
const payload = verifyToken(token)
context.payload = payload as any
return true
} catch (error) {
throw new Error("the valid authorization header is required")
}
}
const readKeyFile = (fileName: string) => readFileSync(join(__dirname, "auth", fileName))

@ -1,59 +0,0 @@
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,24 +0,0 @@
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,
})
Loading…
Cancel
Save