diff --git a/src/api/authorization/index.ts b/src/api/authorization/index.ts deleted file mode 100644 index 063b3e42..00000000 --- a/src/api/authorization/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { shield, and } from 'graphql-shield'; - -import { isAuthenticated } from './isAuthenticated'; -import { isAdmin } from './isAdmin'; -import { isPartner } from './isPartner'; -import { isFreeCourse } from './isFreeCourse'; - -export default shield({ - Query: { - // me: isAuthenticated, - // discountedPrice: isAuthenticated, - - // Partner - - partnerVisitors: and(isAuthenticated, isPartner), - partnerSales: and(isAuthenticated, isPartner), - partnerPayments: and(isAuthenticated, isPartner), - }, - Mutation: { - // passwordChange: isAuthenticated, - // emailChange: isAuthenticated, - communityJoin: isAuthenticated, - - // Admin - - migrate: and(isAuthenticated, isAdmin), - promoteToPartner: and(isAuthenticated, isAdmin), - couponCreate: and(isAuthenticated, isAdmin), - createAdminCourse: and(isAuthenticated, isAdmin), - - // Free - - createFreeCourse: and(isAuthenticated, isFreeCourse), - }, -}); diff --git a/src/api/authorization/isAdmin.ts b/src/api/authorization/isAdmin.ts deleted file mode 100644 index 86c3bb2f..00000000 --- a/src/api/authorization/isAdmin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -import { hasAdminRole } from '@validation/admin'; - -export const isAdmin = rule()(async (_, __, { me }) => { - if (!me) { - return new ForbiddenError('Not authenticated as user.'); - } - - return hasAdminRole(me) - ? true - : new ForbiddenError('No admin user.'); -}); diff --git a/src/api/authorization/isAuthenticated.ts b/src/api/authorization/isAuthenticated.ts deleted file mode 100644 index 038a336e..00000000 --- a/src/api/authorization/isAuthenticated.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -export const isAuthenticated = rule()(async (_, __, { me }) => { - return me ? true : new ForbiddenError('Not authenticated as user.'); -}); diff --git a/src/api/authorization/isFreeCourse.ts b/src/api/authorization/isFreeCourse.ts deleted file mode 100644 index 6944e98a..00000000 --- a/src/api/authorization/isFreeCourse.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { rule } from 'graphql-shield'; - -import storefront from '@data/course-storefront'; -import { COURSE } from '@data/course-keys-types'; -import { BUNDLE } from '@data/bundle-keys-types'; - -export const isFreeCourse = rule()( - async ( - _, - { courseId, bundleId }: { courseId: COURSE; bundleId: BUNDLE } - ) => { - const course = storefront[courseId]; - const bundle = course.bundles[bundleId]; - - return bundle.price === 0 - ? true - : new Error('This course is not for free.'); - } -); diff --git a/src/api/authorization/isPartner.ts b/src/api/authorization/isPartner.ts deleted file mode 100644 index 787ee7e2..00000000 --- a/src/api/authorization/isPartner.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { rule } from 'graphql-shield'; -import { ForbiddenError } from 'apollo-server'; - -import { hasPartnerRole } from '@validation/partner'; - -export const isPartner = rule()(async (_, __, { me }) => { - if (!me) { - return new ForbiddenError('Not authenticated as user.'); - } - - return hasPartnerRole(me) - ? true - : new ForbiddenError('No partner user.'); -}); diff --git a/src/api/middleware/resolver/isFreeCourse.ts b/src/api/middleware/resolver/isFreeCourse.ts new file mode 100644 index 00000000..1a1fdb2a --- /dev/null +++ b/src/api/middleware/resolver/isFreeCourse.ts @@ -0,0 +1,20 @@ +import { MiddlewareFn } from 'type-graphql'; + +import { ResolverContext } from '@typeDefs/resolver'; +import storefront from '@data/course-storefront'; +import { COURSE } from '@data/course-keys-types'; +import { BUNDLE } from '@data/bundle-keys-types'; + +export const isFreeCourse: MiddlewareFn = async ( + { args }, + next +) => { + const course = storefront[args.courseId as COURSE]; + const bundle = course.bundles[args.bundleId as BUNDLE]; + + if (bundle.price !== 0) { + throw new Error('This course is not for free.'); + } + + return next(); +}; diff --git a/src/api/middleware/resolver/isPartner.ts b/src/api/middleware/resolver/isPartner.ts new file mode 100644 index 00000000..dcfbe3a7 --- /dev/null +++ b/src/api/middleware/resolver/isPartner.ts @@ -0,0 +1,20 @@ +import { MiddlewareFn } from 'type-graphql'; +import { ForbiddenError } from 'apollo-server'; + +import { ResolverContext } from '@typeDefs/resolver'; +import { hasPartnerRole } from '@validation/partner'; + +export const isPartner: MiddlewareFn = async ( + { context }, + next +) => { + if (!context.me) { + throw new ForbiddenError('Not authenticated as user.'); + } + + if (!hasPartnerRole(context.me)) { + throw new Error('No partner user.'); + } + + return next(); +}; diff --git a/src/api/resolvers/book/index.ts b/src/api/resolvers/book/index.ts index 5b4d625b..66dbf777 100644 --- a/src/api/resolvers/book/index.ts +++ b/src/api/resolvers/book/index.ts @@ -6,7 +6,9 @@ import { Arg, Resolver, Query, + UseMiddleware, } from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @ObjectType() class File { @@ -29,35 +31,45 @@ class Markdown { @Resolver() export default class BookResolver { @Query(() => File) + @UseMiddleware(isAuthenticated) async book( @Arg('path') path: string, @Arg('fileName') fileName: string - ) { - const data = await s3 + ): Promise { + const { ContentType, Body } = await s3 .getObject({ Bucket: bucket, Key: path, }) .promise(); + if (!ContentType || !Body) { + throw new Error("Book couldn't get downloaded."); + } + return { fileName, - contentType: data.ContentType, - body: data?.Body?.toString('base64'), + contentType: ContentType, + body: Body.toString('base64'), }; } @Query(() => Markdown) - async onlineChapter(@Arg('path') path: string) { - const data = await s3 + @UseMiddleware(isAuthenticated) + async onlineChapter(@Arg('path') path: string): Promise { + const { Body } = await s3 .getObject({ Bucket: bucket, Key: path, }) .promise(); + if (!Body) { + throw new Error("Chapter couldn't get downloaded."); + } + return { - body: data?.Body?.toString('base64'), + body: Body.toString('base64'), }; } } diff --git a/src/api/resolvers/community/index.ts b/src/api/resolvers/community/index.ts index 80925efc..91e31521 100644 --- a/src/api/resolvers/community/index.ts +++ b/src/api/resolvers/community/index.ts @@ -1,6 +1,7 @@ -import { Arg, Resolver, Mutation } from 'type-graphql'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; import { inviteToSlack } from '@services/slack'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; // https://api.slack.com/methods/admin.users.invite const SLACK_ERRORS: { [key: string]: string } = { @@ -61,19 +62,20 @@ const SLACK_ERRORS: { [key: string]: string } = { @Resolver() export default class CommunityResolver { @Mutation(() => Boolean) - async communityJoin(@Arg('email') email: string) { + @UseMiddleware(isAuthenticated) + async communityJoin(@Arg('email') email: string): Promise { try { const result = await inviteToSlack(email); if (!result) { - return new Error('Something went wrong.'); + throw new Error('Something went wrong.'); } if (!result.data.ok) { - return new Error(SLACK_ERRORS[result.data.error]); + throw new Error(SLACK_ERRORS[result.data.error]); } } catch (error) { - return new Error(error); + throw new Error(error); } return true; diff --git a/src/api/resolvers/course/index.ts b/src/api/resolvers/course/index.ts index 2d4e6a09..c53a294b 100644 --- a/src/api/resolvers/course/index.ts +++ b/src/api/resolvers/course/index.ts @@ -6,6 +6,7 @@ import { Resolver, Query, Mutation, + UseMiddleware, } from 'type-graphql'; import { StorefrontCourse } from '@api/resolvers/storefront'; @@ -14,6 +15,9 @@ import { createCourse } from '@services/firebase/course'; import { mergeCourses } from '@services/course'; import { COURSE } from '@data/course-keys-types'; import { BUNDLE } from '@data/bundle-keys-types'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isFreeCourse } from '@api/middleware/resolver/isFreeCourse'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; @ObjectType() class CurriculumItem { @@ -229,17 +233,14 @@ export default class CourseResolver { })); } - @Query(() => UnlockedCourse, { nullable: true }) + @Query(() => UnlockedCourse) + @UseMiddleware(isAuthenticated) async unlockedCourse( @Arg('courseId') courseId: string, @Ctx() ctx: ResolverContext - ): Promise { - if (!ctx.me) { - return null; - } - + ): Promise { const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( - ctx.me.uid, + ctx.me!.uid, courseId as COURSE ); @@ -253,6 +254,7 @@ export default class CourseResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isFreeCourse) async createFreeCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, @@ -287,6 +289,7 @@ export default class CourseResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) async createAdminCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string, diff --git a/src/api/resolvers/migration/index.ts b/src/api/resolvers/migration/index.ts index 5d57bfcf..c1e1d4c1 100644 --- a/src/api/resolvers/migration/index.ts +++ b/src/api/resolvers/migration/index.ts @@ -1,9 +1,14 @@ -import { Arg, Resolver, Mutation } from 'type-graphql'; +import { Arg, Resolver, Mutation, UseMiddleware } from 'type-graphql'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; @Resolver() export default class MigrationResolver { @Mutation(() => Boolean) - async migrate(@Arg('migrationType') migrationType: string) { + @UseMiddleware(isAuthenticated, isAdmin) + async migrate( + @Arg('migrationType') migrationType: string + ): Promise { switch (migrationType) { case 'FOO': return true; diff --git a/src/api/resolvers/partner/index.ts b/src/api/resolvers/partner/index.ts index eb4f7b41..e6902dd7 100644 --- a/src/api/resolvers/partner/index.ts +++ b/src/api/resolvers/partner/index.ts @@ -6,10 +6,14 @@ import { Resolver, Query, Mutation, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; import { hasPartnerRole } from '@validation/partner'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; +import { isAdmin } from '@api/middleware/resolver/isAdmin'; +import { isPartner } from '@api/middleware/resolver/isPartner'; @ObjectType() export class VisitorByDay { @@ -70,11 +74,12 @@ export class PartnerPayment { @Resolver() export default class PartnerResolver { @Query(() => [VisitorByDay]) + @UseMiddleware(isAuthenticated, isPartner) async partnerVisitors( @Arg('from') from: Date, @Arg('to') to: Date, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { return await ctx.partnerConnector.getVisitorsBetweenAggregatedByDate( from, @@ -86,21 +91,18 @@ export default class PartnerResolver { } @Query(() => PartnerSaleConnection) + @UseMiddleware(isAuthenticated, isPartner) async partnerSales( @Arg('offset') offset: number, @Arg('limit') limit: number, @Ctx() ctx: ResolverContext - ) { - if (!ctx.me) { - return []; - } - + ): Promise { try { const { edges, total, } = await ctx.partnerConnector.getSalesByPartner( - ctx.me.uid, + ctx.me!.uid, offset, limit ); @@ -120,19 +122,18 @@ export default class PartnerResolver { }, }; } catch (error) { - return []; + throw new Error(error); } } @Query(() => [PartnerPayment]) - async partnerPayments(@Ctx() ctx: ResolverContext) { - if (!ctx.me) { - return []; - } - + @UseMiddleware(isAuthenticated, isPartner) + async partnerPayments( + @Ctx() ctx: ResolverContext + ): Promise { try { return await ctx.partnerConnector.getPaymentsByPartner( - ctx.me.uid + ctx.me!.uid ); } catch (error) { return []; @@ -140,10 +141,11 @@ export default class PartnerResolver { } @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated, isAdmin) async promoteToPartner( @Arg('uid') uid: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { await ctx.adminConnector.setCustomClaims(uid, { partner: true, @@ -159,7 +161,7 @@ export default class PartnerResolver { async partnerTrackVisitor( @Arg('partnerId') partnerId: string, @Ctx() ctx: ResolverContext - ) { + ): Promise { try { const partner = await ctx.adminConnector.getUser(partnerId); diff --git a/src/api/resolvers/storefront/index.ts b/src/api/resolvers/storefront/index.ts index c1fceb4b..fdc04272 100644 --- a/src/api/resolvers/storefront/index.ts +++ b/src/api/resolvers/storefront/index.ts @@ -49,7 +49,7 @@ export class StorefrontCourse { canUpgrade: boolean; @Field() - bundle: StorefrontBundle; + bundle?: StorefrontBundle; } @Resolver() @@ -58,7 +58,7 @@ export default class StorefrontResolver { async storefrontCourse( @Arg('courseId') courseId: string, @Arg('bundleId') bundleId: string - ) { + ): Promise { const course = storefront[courseId as COURSE]; const bundle = course.bundles[bundleId as BUNDLE]; @@ -74,7 +74,7 @@ export default class StorefrontResolver { } @Query(() => [StorefrontCourse]) - async storefrontCourses() { + async storefrontCourses(): Promise { return Object.values(storefront).map(storefrontCourse => ({ courseId: storefrontCourse.courseId, header: storefrontCourse.header, @@ -85,7 +85,9 @@ export default class StorefrontResolver { } @Query(() => [StorefrontBundle]) - async storefrontBundles(@Arg('courseId') courseId: string) { + async storefrontBundles( + @Arg('courseId') courseId: string + ): Promise { const course = storefront[courseId as COURSE]; return sortBy( diff --git a/src/api/resolvers/stripe/index.ts b/src/api/resolvers/stripe/index.ts index b6978b02..beb5f3d1 100644 --- a/src/api/resolvers/stripe/index.ts +++ b/src/api/resolvers/stripe/index.ts @@ -5,6 +5,7 @@ import { Ctx, Resolver, Mutation, + UseMiddleware, } from 'type-graphql'; import { COURSE } from '@data/course-keys-types'; @@ -21,6 +22,7 @@ import { priceWithDiscount } from '@services/discount'; import stripe from '@services/stripe'; import storefront from '@data/course-storefront'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; // https://stripe.com/docs/payments/checkout/one-time#create-one-time-payments @Resolver() diff --git a/src/api/resolvers/upgrade/index.ts b/src/api/resolvers/upgrade/index.ts index 5c4e51d8..80f23375 100644 --- a/src/api/resolvers/upgrade/index.ts +++ b/src/api/resolvers/upgrade/index.ts @@ -1,23 +1,27 @@ -import { Arg, Ctx, Resolver, Query } from 'type-graphql'; +import { + Arg, + Ctx, + Resolver, + Query, + UseMiddleware, +} from 'type-graphql'; import { StorefrontCourse } from '@api/resolvers/storefront'; import { ResolverContext } from '@typeDefs/resolver'; import { getUpgradeableCourses } from '@services/course'; import { COURSE } from '@data/course-keys-types'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @Resolver() export default class UpgradeResolver { @Query(() => [StorefrontCourse]) + @UseMiddleware(isAuthenticated) async upgradeableCourses( @Arg('courseId') courseId: string, @Ctx() ctx: ResolverContext ) { - if (!ctx.me) { - return []; - } - const courses = await ctx.courseConnector.getCoursesByUserIdAndCourseId( - ctx.me.uid, + ctx.me!.uid, courseId as COURSE ); diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 6347986e..a88b08e9 100644 --- a/src/generated/client.tsx +++ b/src/generated/client.tsx @@ -129,7 +129,7 @@ export type Mutation = { stripeCreateOrder: StripeId; createFreeCourse: Scalars['Boolean']; createAdminCourse: Scalars['Boolean']; - couponCreate?: Maybe; + couponCreate: Scalars['Boolean']; promoteToPartner: Scalars['Boolean']; partnerTrackVisitor: Scalars['Boolean']; communityJoin: Scalars['Boolean']; @@ -284,7 +284,7 @@ export type Query = { storefrontCourses: Array; storefrontBundles: Array; unlockedCourses: Array; - unlockedCourse?: Maybe; + unlockedCourse: UnlockedCourse; book: File; onlineChapter: Markdown; upgradeableCourses: Array; @@ -391,8 +391,8 @@ export type UnlockedCourse = { export type User = { __typename?: 'User'; - email: Scalars['String']; uid: Scalars['String']; + email: Scalars['String']; username: Scalars['String']; roles: Array; }; @@ -485,7 +485,7 @@ export type GetCourseQueryVariables = { export type GetCourseQuery = ( { __typename?: 'Query' } - & { unlockedCourse?: Maybe<( + & { unlockedCourse: ( { __typename?: 'UnlockedCourse' } & Pick & { introduction?: Maybe<( @@ -544,7 +544,7 @@ export type GetCourseQuery = ( )> } )> } )> } - )> } + ) } ); export type CreateFreeCourseMutationVariables = {