From 2a17f3a00db181cbf232ded2f6c17dee1c615d91 Mon Sep 17 00:00:00 2001
From: Daniel Gerdes <daniel.gerdes@ruhr-uni-bochum.de>
Date: Wed, 1 Sep 2021 16:17:34 +0200
Subject: [PATCH] Added TokenCodes and Endpoints for redeeming and requesting

---
 src/app.module.ts                       |   4 +-
 src/auth/agent-auth.guard.ts            |  27 ++++++
 src/auth/graphql-auth.guard.ts          |   8 +-
 src/main.ts                             |   3 +-
 src/resolvers/agent.resolver.ts         |   4 +-
 src/resolvers/entry.resolver.ts         |   4 +-
 src/resolvers/group.resolver.ts         |   6 +-
 src/resolvers/models/agent.model.ts     |   4 +
 src/resolvers/models/tokenCode.model.ts |  15 ++++
 src/resolvers/resolver.module.ts        |   3 +-
 src/resolvers/tokenCode.resolver.ts     | 113 ++++++++++++++++++++++++
 11 files changed, 175 insertions(+), 16 deletions(-)
 create mode 100644 src/auth/agent-auth.guard.ts
 create mode 100644 src/resolvers/models/tokenCode.model.ts
 create mode 100644 src/resolvers/tokenCode.resolver.ts

diff --git a/src/app.module.ts b/src/app.module.ts
index 5f842a2..7c7d319 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -3,7 +3,7 @@ import { PrismaModule } from './prisma/prisma.module';
 import { TelegramModule } from './telegram/telegram.module';
 import { GraphQLModule } from '@nestjs/graphql';
 import { ResolverModule } from './resolvers/resolver.module';
-import { GraphQLAuthGuard } from './auth/graphql-auth.guard';
+import { GroupAuthGuard } from './auth/graphql-auth.guard';
 import { ImageController } from './images.controller';
 import { WebAuthGuard } from './auth/web-auth.guard';
 
@@ -21,7 +21,7 @@ import { WebAuthGuard } from './auth/web-auth.guard';
             }),
         }),
     ],
-    providers: [GraphQLAuthGuard, WebAuthGuard],
+    providers: [GroupAuthGuard, WebAuthGuard],
     controllers: [ImageController],
 })
 export class AppModule {}
diff --git a/src/auth/agent-auth.guard.ts b/src/auth/agent-auth.guard.ts
new file mode 100644
index 0000000..ebabd4b
--- /dev/null
+++ b/src/auth/agent-auth.guard.ts
@@ -0,0 +1,27 @@
+import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import { Request } from 'express';
+import { PrismaService } from '../prisma/prisma.service';
+
+@Injectable()
+export class AgentAuthGuard implements CanActivate {
+    constructor(private prismaService: PrismaService) {}
+
+    async canActivate(context: ExecutionContext): Promise<boolean> {
+        const ctx = GqlExecutionContext.create(context);
+
+        const req: Request = ctx.getContext().req;
+
+        const token = req.headers.authorization;
+
+        if (!token) return false;
+
+        req.agent = await this.prismaService.agent.findFirst({
+            where: { id: token },
+        });
+
+        if (!req.agent) return false;
+
+        return true;
+    }
+}
diff --git a/src/auth/graphql-auth.guard.ts b/src/auth/graphql-auth.guard.ts
index 0eaa070..95ae5a8 100644
--- a/src/auth/graphql-auth.guard.ts
+++ b/src/auth/graphql-auth.guard.ts
@@ -4,7 +4,7 @@ import { Request } from 'express';
 import { PrismaService } from '../prisma/prisma.service';
 
 @Injectable()
-export class GraphQLAuthGuard implements CanActivate {
+export class GroupAuthGuard implements CanActivate {
     constructor(private prismaService: PrismaService) {}
 
     async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -16,13 +16,11 @@ export class GraphQLAuthGuard implements CanActivate {
 
         if (!token) return false;
 
-        const group = await this.prismaService.group.findFirst({
+        req.group = await this.prismaService.group.findFirst({
             where: { code: token },
         });
 
-        if (!group) return false;
-
-        req.group = group;
+        if (!req.group) return false;
 
         return true;
     }
diff --git a/src/main.ts b/src/main.ts
index 4c1caf3..66614b2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,10 @@
 import { NestFactory } from '@nestjs/core';
-import { Group } from '@prisma/client';
+import { Group, Agent, TokenCode } from '@prisma/client';
 
 declare module 'express' {
     interface Request {
         group: Group;
+        agent: Agent;
     }
 }
 
diff --git a/src/resolvers/agent.resolver.ts b/src/resolvers/agent.resolver.ts
index 6b7b65b..843b0fe 100644
--- a/src/resolvers/agent.resolver.ts
+++ b/src/resolvers/agent.resolver.ts
@@ -10,11 +10,11 @@ import { Agent } from './models/agent.model';
 import { PrismaService } from '../prisma/prisma.service';
 import { GraphQLString } from 'graphql';
 import { NotFoundException, UseGuards } from '@nestjs/common';
-import { GraphQLAuthGuard } from '../auth/graphql-auth.guard';
+import { GroupAuthGuard } from '../auth/graphql-auth.guard';
 import { Request } from 'express';
 
 @Resolver(() => Agent)
-@UseGuards(GraphQLAuthGuard)
+@UseGuards(GroupAuthGuard)
 export class AgentResolver {
     constructor(private prismaService: PrismaService) {}
 
diff --git a/src/resolvers/entry.resolver.ts b/src/resolvers/entry.resolver.ts
index 58910d2..22af1ec 100644
--- a/src/resolvers/entry.resolver.ts
+++ b/src/resolvers/entry.resolver.ts
@@ -16,7 +16,7 @@ import {
     UseGuards,
 } from '@nestjs/common';
 import { Request } from 'express';
-import { GraphQLAuthGuard } from '../auth/graphql-auth.guard';
+import { GroupAuthGuard } from '../auth/graphql-auth.guard';
 import { pubSub } from '../pubSub.instance';
 
 @Resolver(() => Entry)
@@ -38,7 +38,7 @@ export class EntryResolver {
         return pubSub.asyncIterator('newEntry');
     }
 
-    @UseGuards(GraphQLAuthGuard)
+    @UseGuards(GroupAuthGuard)
     @Mutation(() => Entry)
     async unlockEntry(
         @Args({ name: 'id', type: () => GraphQLString }) id: string,
diff --git a/src/resolvers/group.resolver.ts b/src/resolvers/group.resolver.ts
index 7f58805..4bc0309 100644
--- a/src/resolvers/group.resolver.ts
+++ b/src/resolvers/group.resolver.ts
@@ -13,7 +13,7 @@ import { PrismaService } from '../prisma/prisma.service';
 import { GraphQLString } from 'graphql';
 import * as seedWords from 'mnemonic-words';
 import { NotFoundException, UseGuards } from '@nestjs/common';
-import { GraphQLAuthGuard } from '../auth/graphql-auth.guard';
+import { GroupAuthGuard } from '../auth/graphql-auth.guard';
 import { Request } from 'express';
 import { pubSub } from '../pubSub.instance';
 
@@ -51,7 +51,7 @@ export class GroupResolver {
     }
 
     @Mutation(() => Group)
-    @UseGuards(GraphQLAuthGuard)
+    //@UseGuards(GraphQLAuthGuard)
     async createGroup(
         @Args({ name: 'name', type: () => GraphQLString }) name: string,
     ) {
@@ -75,7 +75,7 @@ export class GroupResolver {
     }
 
     @Query(() => Group)
-    @UseGuards(GraphQLAuthGuard)
+    @UseGuards(GroupAuthGuard)
     async getGroup(@Context() { req }: { req: Request }) {
         return req.group;
     }
diff --git a/src/resolvers/models/agent.model.ts b/src/resolvers/models/agent.model.ts
index 7caa592..db0fa06 100644
--- a/src/resolvers/models/agent.model.ts
+++ b/src/resolvers/models/agent.model.ts
@@ -1,6 +1,7 @@
 import { Field, ObjectType } from '@nestjs/graphql';
 import { Entry } from './entry.model';
 import { GraphQLBoolean } from 'graphql';
+import { TokenCode } from './tokenCode.model';
 
 @ObjectType()
 export class Agent {
@@ -18,4 +19,7 @@ export class Agent {
 
     @Field(() => [Entry])
     entries: Entry[];
+
+    @Field(() => TokenCode, { nullable: true })
+    tokenCode?: TokenCode;
 }
diff --git a/src/resolvers/models/tokenCode.model.ts b/src/resolvers/models/tokenCode.model.ts
new file mode 100644
index 0000000..fb36c17
--- /dev/null
+++ b/src/resolvers/models/tokenCode.model.ts
@@ -0,0 +1,15 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+import { GraphQLInt } from 'graphql';
+import { Agent } from './agent.model';
+
+@ObjectType()
+export class TokenCode {
+    @Field()
+    id: string;
+
+    @Field(() => GraphQLInt)
+    value: number;
+
+    @Field(() => Agent)
+    agent: Agent;
+}
\ No newline at end of file
diff --git a/src/resolvers/resolver.module.ts b/src/resolvers/resolver.module.ts
index 26a64a6..dba4f82 100644
--- a/src/resolvers/resolver.module.ts
+++ b/src/resolvers/resolver.module.ts
@@ -2,8 +2,9 @@ import { Module } from '@nestjs/common';
 import { GroupResolver } from './group.resolver';
 import { EntryResolver } from './entry.resolver';
 import { AgentResolver } from './agent.resolver';
+import { TokenCodeResolver } from './tokenCode.resolver';
 
 @Module({
-    providers: [GroupResolver, EntryResolver, AgentResolver],
+    providers: [GroupResolver, EntryResolver, AgentResolver, TokenCodeResolver],
 })
 export class ResolverModule {}
diff --git a/src/resolvers/tokenCode.resolver.ts b/src/resolvers/tokenCode.resolver.ts
new file mode 100644
index 0000000..73bfd95
--- /dev/null
+++ b/src/resolvers/tokenCode.resolver.ts
@@ -0,0 +1,113 @@
+import { UseGuards, NotFoundException } from "@nestjs/common";
+import { Args, Context, Mutation, Parent, ResolveField, Resolver, Subscription } from "@nestjs/graphql";
+import { GraphQLString, GraphQLInt } from 'graphql';
+import { Request } from "express";
+import { GroupAuthGuard } from "../auth/graphql-auth.guard";
+import { PrismaService } from "../prisma/prisma.service";
+import { TokenCode } from "./models/tokenCode.model";
+import { AgentAuthGuard } from "../auth/agent-auth.guard";
+import { pubSub } from "../pubSub.instance";
+
+@Resolver(() => TokenCode)
+export class TokenCodeResolver {
+    constructor(private prismaService: PrismaService) {}
+
+    // TODO: protect subscription
+    @Subscription(() => TokenCode, {
+        filter: (payload, variables) =>
+            payload.newTokenCode.agent.id === variables.id,
+    })
+    newTokenCode(@Args('id') id: string){
+        return pubSub.asyncIterator('newTokenCode');
+    }
+
+    @UseGuards(AgentAuthGuard)
+    @Mutation(() => TokenCode)
+    async tokenCode(@Context() { req }: { req: Request }) {
+        let tokenCode = await this.prismaService.tokenCode.findFirst({
+            where: {
+                id: req.agent.tokenCodeId,
+            },
+        });
+
+        if (!tokenCode) {
+            tokenCode = await this.prismaService.tokenCode.create({
+                data: {
+                    value: 1,
+                    agent: {
+                        connect: { id: req.agent.id },
+                    },
+                },
+            });
+            
+            pubSub.publish('newTokenCode', {
+                newTokenCode: tokenCode
+            });
+        }
+
+        return tokenCode;
+    }
+
+    @UseGuards(GroupAuthGuard)
+    @Mutation(() => GraphQLInt)
+    async redeemCode(
+        @Args({ name: 'code', type: () => GraphQLString }) code: string,
+        @Context() { req }: { req: Request },
+    ) {
+        const tokenCode = await this.prismaService.tokenCode.findFirst({
+            where: { id: code },
+            include: {
+                agent: true,
+            }
+        });
+
+        if (!tokenCode)
+            throw new NotFoundException();
+
+        
+        const newTokenCode = await this.prismaService.tokenCode.create({
+            data: {
+                value: 1,
+                agent: {
+                    connect: { id: tokenCode.agent.id },
+                },
+            },
+            include: {
+                agent: true,
+            }
+        });
+
+        pubSub.publish('newTokenCode', {
+            newTokenCode: newTokenCode,
+        });
+
+        await this.prismaService.tokenCode.delete({
+            where: { id: tokenCode.id },
+        });
+
+        req.group = await this.prismaService.group.update({
+            where: { id: req.group.id },
+            data: {
+                tokens: {
+                    increment: tokenCode.value,
+                },
+            },
+        });
+
+        pubSub.publish('updateToken', {
+            updateToken: req.group,
+        });
+
+        return tokenCode.value;
+    }
+
+    @ResolveField('agent')
+    agent(@Parent() token: TokenCode) {
+        return this.prismaService.agent.findFirst({
+            where: {
+                tokenCodeId: token.id,
+            },
+        });
+    }
+
+}
\ No newline at end of file
-- 
GitLab