Sfoglia il codice sorgente

Kill switch feature (#38)

Rafał Pawłow 2 anni fa
parent
commit
5b323f7457

+ 8 - 0
schema.graphql

@@ -3,6 +3,10 @@
 # !!!   DO NOT MODIFY THIS FILE BY YOURSELF   !!!
 # -----------------------------------------------
 
+type Admin {
+  isKilled: Boolean!
+}
+
 type CategoryFeaturedVideos {
   categoryFeaturedVideos: [FeaturedVideo!]!
   categoryId: ID!
@@ -43,6 +47,7 @@ type Mutation {
   """Report a video"""
   reportVideo(rationale: String!, videoId: ID!): ReportVideoInfo!
   setCategoryFeaturedVideos(categoryId: ID!, videos: [FeaturedVideoInput!]!): [FeaturedVideo!]!
+  setKillSwitch(isKilled: Boolean!): Admin!
   setVideoHero(newVideoHero: VideoHeroInput!): VideoHero!
 
   """Remove a single follow from the target channel"""
@@ -50,6 +55,9 @@ type Mutation {
 }
 
 type Query {
+  """Set killed instance"""
+  admin: Admin!
+
   """Get featured videos for all categories"""
   allCategoriesFeaturedVideos(videosLimit: Int!): [CategoryFeaturedVideos!]!
 

+ 7 - 0
src/config.ts

@@ -1,6 +1,7 @@
 import dotenv from 'dotenv'
 
 const isDev = process.env.NODE_ENV === 'development'
+export const ADMIN_ROLE = 'ADMIN'
 
 type LoadEnvVarOpts = {
   defaultValue?: string
@@ -27,6 +28,7 @@ export class Config {
   private _port: number
   private _mongoDBUri: string
   private _featuredContentSecret: string
+  private _adminSecret: string
   private _queryNodeUrl: string
   private _isDebugging: boolean
 
@@ -42,6 +44,10 @@ export class Config {
     return this._featuredContentSecret
   }
 
+  get adminSecret(): string {
+    return this._adminSecret
+  }
+
   get queryNodeUrl(): string {
     return this._queryNodeUrl
   }
@@ -63,6 +69,7 @@ export class Config {
     this._mongoDBUri = `mongodb://${mongoHostname}:${rawMongoPort}/${mongoDatabase}`
 
     this._featuredContentSecret = loadEnvVar('ORION_FEATURED_CONTENT_SECRET')
+    this._adminSecret = loadEnvVar('ORION_ADMIN_SECRET')
     this._queryNodeUrl = loadEnvVar('ORION_QUERY_NODE_URL')
 
     this._isDebugging = loadEnvVar('ORION_DEBUGGING', { defaultValue: 'false' }) === 'true'

+ 5 - 2
src/helpers/auth.ts

@@ -1,7 +1,10 @@
 import { AuthChecker } from 'type-graphql'
 import { OrionContext } from '../types'
-import config from '../config'
+import config, { ADMIN_ROLE } from '../config'
 
-export const customAuthChecker: AuthChecker<OrionContext> = ({ context }) => {
+export const customAuthChecker: AuthChecker<OrionContext> = ({ context }, roles) => {
+  if (roles.includes(ADMIN_ROLE)) {
+    return context.authorization === config.adminSecret
+  }
   return context.authorization === config.featuredContentSecret
 }

+ 20 - 0
src/models/Admin.ts

@@ -0,0 +1,20 @@
+import { DocumentType, getModelForClass, prop } from '@typegoose/typegoose'
+import { ArgsType, ObjectType, Field } from 'type-graphql'
+
+@ObjectType()
+@ArgsType()
+export class Admin {
+  @prop({ required: true })
+  @Field()
+  isKilled: boolean
+}
+
+export const AdminModel = getModelForClass(Admin, { schemaOptions: { collection: 'admin' } })
+
+export const getAdminDoc = async (): Promise<DocumentType<Admin>> => {
+  const document = await AdminModel.findOne()
+  if (!document) {
+    return await AdminModel.create({ isKilled: false })
+  }
+  return document
+}

+ 26 - 0
src/resolvers/admin.ts

@@ -0,0 +1,26 @@
+import { Authorized, Field, Mutation, Query, Resolver, Args, ArgsType } from 'type-graphql'
+import { getAdminDoc, Admin } from '../models/Admin'
+import { ADMIN_ROLE } from '../config'
+
+@ArgsType()
+class AdminInput implements Admin {
+  @Field()
+  isKilled: boolean
+}
+
+@Resolver()
+export class AdminResolver {
+  @Query(() => Admin, { nullable: false, description: 'Set killed instance' })
+  async admin() {
+    return await getAdminDoc()
+  }
+
+  @Mutation(() => Admin, { nullable: false })
+  @Authorized(ADMIN_ROLE)
+  async setKillSwitch(@Args() { isKilled }: AdminInput) {
+    const killSwitch = await getAdminDoc()
+    killSwitch.isKilled = isKilled
+    await killSwitch.save()
+    return { isKilled }
+  }
+}

+ 8 - 1
src/resolvers/index.ts

@@ -2,5 +2,12 @@ import { FeaturedContentResolver } from './featuredContent'
 import { ChannelFollowsInfosResolver } from './followsInfo'
 import { ReportsInfosResolver } from './reportsInfo'
 import { VideoViewsInfosResolver } from './viewsInfo'
+import { AdminResolver } from './admin'
 
-export { ChannelFollowsInfosResolver, VideoViewsInfosResolver, FeaturedContentResolver, ReportsInfosResolver }
+export {
+  ChannelFollowsInfosResolver,
+  VideoViewsInfosResolver,
+  FeaturedContentResolver,
+  ReportsInfosResolver,
+  AdminResolver,
+}

+ 8 - 1
src/server.ts

@@ -16,6 +16,7 @@ import {
   VideoViewsInfosResolver,
   FeaturedContentResolver,
   ReportsInfosResolver,
+  AdminResolver,
 } from './resolvers'
 import { queryNodeStitchingResolvers } from './resolvers/queryNodeStitchingResolvers'
 import { Aggregates, OrionContext } from './types'
@@ -30,7 +31,13 @@ export const createServer = async (mongoose: Mongoose, aggregates: Aggregates, q
   })
 
   const orionSchema = await buildSchema({
-    resolvers: [VideoViewsInfosResolver, ChannelFollowsInfosResolver, ReportsInfosResolver, FeaturedContentResolver],
+    resolvers: [
+      VideoViewsInfosResolver,
+      ChannelFollowsInfosResolver,
+      ReportsInfosResolver,
+      FeaturedContentResolver,
+      AdminResolver,
+    ],
     authChecker: customAuthChecker,
     emitSchemaFile: 'schema.graphql',
     validate: true,

+ 50 - 0
tests/admin.test.ts

@@ -0,0 +1,50 @@
+import { ApolloServer } from 'apollo-server-express'
+import { Mongoose } from 'mongoose'
+import { Aggregates } from '../src/types'
+import { createMutationFn, createQueryFn, MutationFn, QueryFn } from './helpers'
+import { buildAggregates, connectMongoose, createServer } from '../src/server'
+
+import { GetKillSwitch, GET_KILL_SWITCH, SetKillSwitch, SET_KILL_SWITCH, SetKillSwitchArgs } from './queries/admin'
+import { AdminModel } from '../src/models/Admin'
+
+describe('Kill switch resolver', () => {
+  let server: ApolloServer
+  let mongoose: Mongoose
+  let aggregates: Aggregates
+  let query: QueryFn
+  let mutate: MutationFn
+
+  beforeEach(async () => {
+    mongoose = await connectMongoose(process.env.MONGO_URL!)
+    aggregates = await buildAggregates()
+    server = await createServer(mongoose, aggregates, process.env.ORION_QUERY_NODE_URL!)
+    await server.start()
+    query = createQueryFn(server)
+    mutate = createMutationFn(server)
+  })
+
+  afterEach(async () => {
+    await server.stop()
+    await AdminModel.deleteMany({})
+    await mongoose.disconnect()
+  })
+
+  const getKillSwitch = async () => {
+    const result = await query<GetKillSwitch>({
+      query: GET_KILL_SWITCH,
+    })
+    expect(result.errors).toBeUndefined()
+    return result.data?.admin.isKilled
+  }
+
+  it('should return isKilled set to false', async () => {
+    const isKilled = await getKillSwitch()
+    expect(isKilled).toEqual(false)
+  })
+
+  it('should set isKilled to true', async () => {
+    await mutate<SetKillSwitch, SetKillSwitchArgs>({ mutation: SET_KILL_SWITCH, variables: { isKilled: true } })
+    const isKilled = await getKillSwitch()
+    expect(isKilled).toEqual(true)
+  })
+})

+ 31 - 0
tests/queries/admin.ts

@@ -0,0 +1,31 @@
+import { gql } from 'apollo-server-express'
+import { Admin } from '../../src/models/Admin'
+
+export const GET_KILL_SWITCH = gql`
+  query GetKillSwitch {
+    admin {
+      isKilled
+    }
+  }
+`
+
+export type GetKillSwitch = {
+  admin: {
+    isKilled: boolean
+  }
+}
+
+export const SET_KILL_SWITCH = gql`
+  mutation SetKillSwitch($isKilled: Boolean!) {
+    setKillSwitch(isKilled: $isKilled) {
+      isKilled
+    }
+  }
+`
+
+export type SetKillSwitch = {
+  setKillSwitch: Admin
+}
+export type SetKillSwitchArgs = {
+  isKilled: boolean
+}