parent
d4a6faad5a
commit
b3128ede9a
@ -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}. ` |
||||
) |
||||
) |
||||
})() |
||||
|
@ -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