parent
d4a6faad5a
commit
b3128ede9a
@ -1,40 +1,41 @@ |
|||||||
{ |
{ |
||||||
"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 -i --watch", |
"test": "jest -i --coverage", |
||||||
"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" |
"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": "", |
"keywords": [], |
||||||
"license": "ISC", |
"author": "", |
||||||
"dependencies": { |
"license": "ISC", |
||||||
"apollo-server": "^2.9.3", |
"dependencies": { |
||||||
"apollo-server-express": "^2.9.4", |
"apollo-server": "^2.9.3", |
||||||
"argon2": "^0.24.1", |
"apollo-server-express": "^2.9.4", |
||||||
"dotenv": "^8.1.0", |
"argon2": "^0.24.1", |
||||||
"express": "^4.17.1", |
"dotenv": "^8.1.0", |
||||||
"graphql": "^14.5.4", |
"express": "^4.17.1", |
||||||
"jsonwebtoken": "^8.5.1", |
"graphql": "^14.5.4", |
||||||
"pg": "^7.12.1", |
"jsonwebtoken": "^8.5.1", |
||||||
"reflect-metadata": "^0.1.13", |
"pg": "^7.12.1", |
||||||
"type-graphql": "^0.17.5", |
"reflect-metadata": "^0.1.13", |
||||||
"typeorm": "^0.2.18" |
"type-graphql": "^0.17.5", |
||||||
}, |
"typeorm": "^0.2.18" |
||||||
"devDependencies": { |
}, |
||||||
"@types/express": "^4.17.1", |
"devDependencies": { |
||||||
"@types/graphql": "^14.5.0", |
"@types/express": "^4.17.1", |
||||||
"@types/jest": "^24.0.18", |
"@types/graphql": "^14.5.0", |
||||||
"@types/js-cookie": "^2.2.2", |
"@types/jest": "^24.0.18", |
||||||
"@types/jsonwebtoken": "^8.3.3", |
"@types/js-cookie": "^2.2.2", |
||||||
"@types/node": "^12.7.5", |
"@types/jsonwebtoken": "^8.3.3", |
||||||
"class-transformer": "^0.2.3", |
"@types/node": "^12.7.5", |
||||||
"jest": "^24.9.0", |
"class-transformer": "^0.2.3", |
||||||
"ts-jest": "^24.1.0", |
"jest": "^24.9.0", |
||||||
"ts-node-dev": "^1.0.0-pre.42", |
"ts-jest": "^24.1.0", |
||||||
"typescript": "^3.6.3" |
"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() |
require("dotenv").config() |
||||||
import { ApolloServer } from "apollo-server-express" |
import { ApolloServer } from "apollo-server-express" |
||||||
import { createConnection } from "typeorm" |
import { createConnection } from "typeorm" |
||||||
import { createSchema } from "./schema" |
import { connectionOptions, createSchema } from "./app/schema" |
||||||
import { User } from "./User" |
|
||||||
import express = require("express") |
import express = require("express") |
||||||
|
|
||||||
;(async () => { |
;(async () => { |
||||||
await createConnection({ |
await createConnection(connectionOptions) |
||||||
type: "postgres", |
|
||||||
host: "localhost", |
|
||||||
port: 5432, |
|
||||||
database: "postgres", |
|
||||||
username: "postgres", |
|
||||||
password: "postgres", |
|
||||||
// dropSchema: true,
|
|
||||||
entities: [User], |
|
||||||
synchronize: true, |
|
||||||
logging: false, |
|
||||||
}) |
|
||||||
|
|
||||||
const server = new ApolloServer({ |
const server = new ApolloServer({ |
||||||
schema: await createSchema(), |
schema: await createSchema(), |
||||||
playground: true, |
playground: true, |
||||||
introspection: true, |
introspection: true, |
||||||
debug: true, |
debug: true, |
||||||
context: ({ req, res }) => ({ req, res }), |
context: ({ req, res }) => ({ req, res }), |
||||||
}) |
}) |
||||||
|
|
||||||
const app = express() |
const app = express() |
||||||
server.applyMiddleware({ app }) |
server.applyMiddleware({ app }) |
||||||
|
|
||||||
const PORT = process.env.PORT || 4000 |
const APP_PORT = process.env.APP_PORT || 4000 |
||||||
app.listen({ port: PORT }, () => |
app.listen({ port: APP_PORT }, () => |
||||||
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}. `) |
console.log( |
||||||
) |
`🚀 Server ready at http://localhost:${APP_PORT}${server.graphqlPath}. ` |
||||||
|
) |
||||||
|
) |
||||||
})() |
})() |
||||||
|
@ -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 = "" |
||||||
|
} |
@ -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…
Reference in new issue