This repository has been archived by the owner on Mar 23, 2023. It is now read-only.
forked from MichalLytek/type-graphql
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { GraphQLResolveInfo, GraphQLObjectType } from "graphql"; | ||
|
||
import { MiddlewareFn } from "../../src"; | ||
import { Context } from "./context.interface"; | ||
import { UnauthorizedError } from "../../src/errors"; | ||
|
||
const extractAuthorizationExtensions = (info: GraphQLResolveInfo) => { | ||
const parentAuthorizationExtensions = | ||
(info.parentType.extensions && info.parentType.extensions.authorization) || {}; | ||
const returnType = info.returnType as GraphQLObjectType; | ||
const returnTypeAuthorizationExtensions = | ||
(returnType.extensions && returnType.extensions.authorization) || {}; | ||
const field = info.parentType.getFields()[info.fieldName]; | ||
const fieldAuthorizationExtensions = (field.extensions && field.extensions.authorization) || {}; | ||
|
||
return { | ||
...parentAuthorizationExtensions, | ||
...returnTypeAuthorizationExtensions, | ||
...fieldAuthorizationExtensions, | ||
}; | ||
}; | ||
|
||
export const AuthorizerMiddleware: MiddlewareFn = async ( | ||
{ context: { user }, info }: { context: Context; info: GraphQLResolveInfo }, | ||
next, | ||
) => { | ||
const { restricted = false, roles = [] } = extractAuthorizationExtensions(info); | ||
|
||
if (restricted) { | ||
if (!user) { | ||
// if no user, restrict access | ||
throw new UnauthorizedError(); | ||
} | ||
|
||
if (roles.length > 0 && !user.roles.some(role => roles.includes(role))) { | ||
// if the roles don't overlap, restrict access | ||
throw new UnauthorizedError(); | ||
} | ||
} | ||
|
||
// grant access in other cases | ||
await next(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { User } from "./user.interface"; | ||
|
||
export interface Context { | ||
user?: User; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Extensions } from "../../src"; | ||
|
||
export const CustomAuthorized = (roles: string | string[] = []) => | ||
Extensions({ | ||
authorization: { | ||
restricted: true, | ||
roles: typeof roles === "string" ? [roles] : roles, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
query GetPublicRecipes { | ||
recipes { | ||
title | ||
description | ||
averageRating | ||
} | ||
} | ||
|
||
query GetRecipesForAuthedUser { | ||
recipes { | ||
title | ||
description | ||
ingredients | ||
averageRating | ||
} | ||
} | ||
|
||
query GetRecipesForAdmin { | ||
recipes { | ||
title | ||
description | ||
ingredients | ||
averageRating | ||
ratings | ||
} | ||
} | ||
|
||
mutation AddRecipeByAuthedUser { | ||
addRecipe(title: "Sample Recipe") { | ||
averageRating | ||
} | ||
} | ||
|
||
mutation DeleteRecipeByAdmin { | ||
deleteRecipe(title: "Recipe 1") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import "reflect-metadata"; | ||
import { ApolloServer } from "apollo-server"; | ||
import { buildSchema } from "../../src"; | ||
|
||
import { ExampleResolver } from "./resolver"; | ||
import { Context } from "./context.interface"; | ||
import { AuthorizerMiddleware } from "./authorizer.middleware"; | ||
import { LoggerMiddleware } from "./logger.middleware"; | ||
|
||
void (async function bootstrap() { | ||
// build TypeGraphQL executable schema | ||
const schema = await buildSchema({ | ||
resolvers: [ExampleResolver], | ||
globalMiddlewares: [AuthorizerMiddleware, LoggerMiddleware], | ||
}); | ||
|
||
// Create GraphQL server | ||
const server = new ApolloServer({ | ||
schema, | ||
context: () => { | ||
const ctx: Context = { | ||
// create mocked user in context | ||
// in real app you would be mapping user from `req.user` or sth | ||
user: { | ||
id: 1, | ||
name: "Sample user", | ||
roles: ["REGULAR"], | ||
}, | ||
}; | ||
return ctx; | ||
}, | ||
}); | ||
|
||
// Start the server | ||
const { url } = await server.listen(4000); | ||
console.log(`Server is running, GraphQL Playground available at ${url}`); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Service } from "typedi"; | ||
import { MiddlewareInterface, NextFn, ResolverData } from "../../src"; | ||
|
||
import { Context } from "./context.interface"; | ||
import { Logger } from "./logger"; | ||
|
||
@Service() | ||
export class LoggerMiddleware implements MiddlewareInterface<Context> { | ||
constructor(private readonly logger: Logger) {} | ||
|
||
async use({ context: { user }, info }: ResolverData<Context>, next: NextFn) { | ||
const { logMessage, logLevel = 0 } = | ||
info.parentType.getFields()[info.fieldName].extensions || {}; | ||
|
||
if (logMessage) { | ||
this.logger.log(`${logMessage}${user ? ` (user: ${user.id})` : ""}`, logLevel); | ||
} | ||
|
||
return next(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Service } from "typedi"; | ||
|
||
@Service() | ||
export class Logger { | ||
log(...args: any[]) { | ||
// replace with more sophisticated solution :) | ||
console.log(...args); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { plainToClass } from "class-transformer"; | ||
|
||
import { Recipe } from "./recipe.type"; | ||
|
||
export function createRecipe(recipeData: Partial<Recipe>): Recipe { | ||
return plainToClass(Recipe, recipeData); | ||
} | ||
|
||
export const sampleRecipes = [ | ||
createRecipe({ | ||
title: "Recipe 1", | ||
description: "Desc 1", | ||
ingredients: ["one", "two", "three"], | ||
ratings: [3, 4, 5, 5, 5], | ||
}), | ||
createRecipe({ | ||
title: "Recipe 2", | ||
description: "Desc 2", | ||
ingredients: ["four", "five", "six"], | ||
ratings: [3, 4, 5, 3, 2], | ||
}), | ||
createRecipe({ | ||
title: "Recipe 3", | ||
ingredients: ["seven", "eight", "nine"], | ||
ratings: [4, 4, 5, 5, 4], | ||
}), | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { ObjectType, Extensions, Field, Int, Float } from "../../src"; | ||
import { CustomAuthorized } from "./custom.authorized"; | ||
|
||
@ObjectType() | ||
@CustomAuthorized() // restrict access to all receipe fields only for logged users | ||
export class Recipe { | ||
@Field() | ||
title: string; | ||
|
||
@Field({ nullable: true }) | ||
description?: string; | ||
|
||
@Field(type => [String]) | ||
@Extensions({ logMessage: "ingredients accessed" }) | ||
@Extensions({ logLevel: 4 }) | ||
ingredients: string[]; | ||
|
||
@CustomAuthorized("ADMIN") // restrict access to rates details for admin only, this will override the object type custom authorization | ||
@Field(type => [Int]) | ||
ratings: number[]; | ||
|
||
@Field(type => Float, { nullable: true }) | ||
get averageRating(): number | null { | ||
return this.ratings.reduce((a, b) => a + b, 0) / this.ratings.length; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Resolver, Query, Mutation, Arg, Extensions } from "../../src"; | ||
import { CustomAuthorized } from "./custom.authorized"; | ||
|
||
import { Recipe } from "./recipe.type"; | ||
import { createRecipe, sampleRecipes } from "./recipe.helpers"; | ||
|
||
@Resolver() | ||
export class ExampleResolver { | ||
private recipesData: Recipe[] = sampleRecipes.slice(); | ||
|
||
@Extensions({ some: "data" }) | ||
@Query(returns => [Recipe]) | ||
async recipes(): Promise<Recipe[]> { | ||
return await this.recipesData; | ||
} | ||
|
||
@CustomAuthorized() // only logged users can add new recipe | ||
@Mutation() | ||
addRecipe( | ||
@Arg("title") title: string, | ||
@Arg("description", { nullable: true }) description?: string, | ||
): Recipe { | ||
const newRecipe = createRecipe({ | ||
title, | ||
description, | ||
ratings: [], | ||
}); | ||
this.recipesData.push(newRecipe); | ||
return newRecipe; | ||
} | ||
|
||
@CustomAuthorized("ADMIN") // only admin can remove the published recipe | ||
@Mutation() | ||
deleteRecipe(@Arg("title") title: string): boolean { | ||
const foundRecipeIndex = this.recipesData.findIndex(it => it.title === title); | ||
if (!foundRecipeIndex) { | ||
return false; | ||
} | ||
this.recipesData.splice(foundRecipeIndex, 1); | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface User { | ||
id: number; | ||
name: string; | ||
roles: string[]; | ||
} |