add generate api key api and test (#392)

* add generate api key api and test

* test if user can make api call with the api key
This commit is contained in:
Hongbo Wu
2022-04-12 12:11:45 +08:00
committed by GitHub
parent cc95574d5c
commit 2ebdaba780
8 changed files with 175 additions and 0 deletions

View File

@ -521,6 +521,22 @@ export type FeedArticlesSuccess = {
pageInfo: PageInfo;
};
export type GenerateApiKeyError = {
__typename?: 'GenerateApiKeyError';
errorCodes: Array<GenerateApiKeyErrorCode>;
};
export enum GenerateApiKeyErrorCode {
BadRequest = 'BAD_REQUEST'
}
export type GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess;
export type GenerateApiKeySuccess = {
__typename?: 'GenerateApiKeySuccess';
apiKey: Scalars['String'];
};
export type GetFollowersError = {
__typename?: 'GetFollowersError';
errorCodes: Array<GetFollowersErrorCode>;
@ -771,6 +787,7 @@ export type Mutation = {
deleteNewsletterEmail: DeleteNewsletterEmailResult;
deleteReaction: DeleteReactionResult;
deleteReminder: DeleteReminderResult;
generateApiKey: GenerateApiKeyResult;
googleLogin: LoginResult;
googleSignup: GoogleSignupResult;
login: LoginResult;
@ -867,6 +884,11 @@ export type MutationDeleteReminderArgs = {
};
export type MutationGenerateApiKeyArgs = {
scope?: InputMaybe<Scalars['String']>;
};
export type MutationGoogleLoginArgs = {
input: GoogleLoginInput;
};
@ -1989,6 +2011,10 @@ export type ResolversTypes = {
FeedArticlesResult: ResolversTypes['FeedArticlesError'] | ResolversTypes['FeedArticlesSuccess'];
FeedArticlesSuccess: ResolverTypeWrapper<FeedArticlesSuccess>;
Float: ResolverTypeWrapper<Scalars['Float']>;
GenerateApiKeyError: ResolverTypeWrapper<GenerateApiKeyError>;
GenerateApiKeyErrorCode: GenerateApiKeyErrorCode;
GenerateApiKeyResult: ResolversTypes['GenerateApiKeyError'] | ResolversTypes['GenerateApiKeySuccess'];
GenerateApiKeySuccess: ResolverTypeWrapper<GenerateApiKeySuccess>;
GetFollowersError: ResolverTypeWrapper<GetFollowersError>;
GetFollowersErrorCode: GetFollowersErrorCode;
GetFollowersResult: ResolversTypes['GetFollowersError'] | ResolversTypes['GetFollowersSuccess'];
@ -2252,6 +2278,9 @@ export type ResolversParentTypes = {
FeedArticlesResult: ResolversParentTypes['FeedArticlesError'] | ResolversParentTypes['FeedArticlesSuccess'];
FeedArticlesSuccess: FeedArticlesSuccess;
Float: Scalars['Float'];
GenerateApiKeyError: GenerateApiKeyError;
GenerateApiKeyResult: ResolversParentTypes['GenerateApiKeyError'] | ResolversParentTypes['GenerateApiKeySuccess'];
GenerateApiKeySuccess: GenerateApiKeySuccess;
GetFollowersError: GetFollowersError;
GetFollowersResult: ResolversParentTypes['GetFollowersError'] | ResolversParentTypes['GetFollowersSuccess'];
GetFollowersSuccess: GetFollowersSuccess;
@ -2763,6 +2792,20 @@ export type FeedArticlesSuccessResolvers<ContextType = ResolverContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GenerateApiKeyErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GenerateApiKeyError'] = ResolversParentTypes['GenerateApiKeyError']> = {
errorCodes?: Resolver<Array<ResolversTypes['GenerateApiKeyErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GenerateApiKeyResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GenerateApiKeyResult'] = ResolversParentTypes['GenerateApiKeyResult']> = {
__resolveType: TypeResolveFn<'GenerateApiKeyError' | 'GenerateApiKeySuccess', ParentType, ContextType>;
};
export type GenerateApiKeySuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GenerateApiKeySuccess'] = ResolversParentTypes['GenerateApiKeySuccess']> = {
apiKey?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type GetFollowersErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['GetFollowersError'] = ResolversParentTypes['GetFollowersError']> = {
errorCodes?: Resolver<Array<ResolversTypes['GetFollowersErrorCode']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@ -2957,6 +3000,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
deleteNewsletterEmail?: Resolver<ResolversTypes['DeleteNewsletterEmailResult'], ParentType, ContextType, RequireFields<MutationDeleteNewsletterEmailArgs, 'newsletterEmailId'>>;
deleteReaction?: Resolver<ResolversTypes['DeleteReactionResult'], ParentType, ContextType, RequireFields<MutationDeleteReactionArgs, 'id'>>;
deleteReminder?: Resolver<ResolversTypes['DeleteReminderResult'], ParentType, ContextType, RequireFields<MutationDeleteReminderArgs, 'id'>>;
generateApiKey?: Resolver<ResolversTypes['GenerateApiKeyResult'], ParentType, ContextType, Partial<MutationGenerateApiKeyArgs>>;
googleLogin?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationGoogleLoginArgs, 'input'>>;
googleSignup?: Resolver<ResolversTypes['GoogleSignupResult'], ParentType, ContextType, RequireFields<MutationGoogleSignupArgs, 'input'>>;
login?: Resolver<ResolversTypes['LoginResult'], ParentType, ContextType, RequireFields<MutationLoginArgs, 'input'>>;
@ -3521,6 +3565,9 @@ export type Resolvers<ContextType = ResolverContext> = {
FeedArticlesError?: FeedArticlesErrorResolvers<ContextType>;
FeedArticlesResult?: FeedArticlesResultResolvers<ContextType>;
FeedArticlesSuccess?: FeedArticlesSuccessResolvers<ContextType>;
GenerateApiKeyError?: GenerateApiKeyErrorResolvers<ContextType>;
GenerateApiKeyResult?: GenerateApiKeyResultResolvers<ContextType>;
GenerateApiKeySuccess?: GenerateApiKeySuccessResolvers<ContextType>;
GetFollowersError?: GetFollowersErrorResolvers<ContextType>;
GetFollowersResult?: GetFollowersResultResolvers<ContextType>;
GetFollowersSuccess?: GetFollowersSuccessResolvers<ContextType>;

View File

@ -457,6 +457,20 @@ type FeedArticlesSuccess {
pageInfo: PageInfo!
}
type GenerateApiKeyError {
errorCodes: [GenerateApiKeyErrorCode!]!
}
enum GenerateApiKeyErrorCode {
BAD_REQUEST
}
union GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess
type GenerateApiKeySuccess {
apiKey: String!
}
type GetFollowersError {
errorCodes: [GetFollowersErrorCode!]!
}
@ -684,6 +698,7 @@ type Mutation {
deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult!
deleteReaction(id: ID!): DeleteReactionResult!
deleteReminder(id: ID!): DeleteReminderResult!
generateApiKey(scope: String): GenerateApiKeyResult!
googleLogin(input: GoogleLoginInput!): LoginResult!
googleSignup(input: GoogleSignupInput!): GoogleSignupResult!
login(input: LoginInput!): LoginResult!

View File

@ -0,0 +1,40 @@
import {
GenerateApiKeyError,
GenerateApiKeyErrorCode,
GenerateApiKeySuccess,
MutationGenerateApiKeyArgs,
} from '../../generated/graphql'
import { generateApiKey } from '../../utils/auth'
import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { authorized } from '../../utils/helpers'
export const generateApiKeyResolver = authorized<
GenerateApiKeySuccess,
GenerateApiKeyError,
MutationGenerateApiKeyArgs
>((_, { scope }, { claims }) => {
try {
console.log('generateApiKeyResolver', scope)
analytics.track({
userId: claims.uid,
event: 'generate_api_key',
properties: {
scope,
env: env.server.apiEnv,
},
})
const apiKey = generateApiKey({
iat: new Date().getTime(),
scope: scope || 'all',
uid: claims.uid,
})
return { apiKey }
} catch (error) {
console.error(error)
return { errorCodes: [GenerateApiKeyErrorCode.BadRequest] }
}
})

View File

@ -79,6 +79,7 @@ import {
generateUploadFilePathName,
} from '../utils/uploads'
import { getPageById, getPageByParam } from '../elastic'
import { generateApiKeyResolver } from './api_key'
/* eslint-disable @typescript-eslint/naming-convention */
type ResultResolveType = {
@ -138,6 +139,7 @@ export const functionResolvers = {
login: loginResolver,
signup: signupResolver,
setLabels: setLabelsResolver,
generateApiKey: generateApiKeyResolver,
},
Query: {
me: getMeUserResolver,
@ -529,4 +531,5 @@ export const functionResolvers = {
...resultResolveTypeResolver('Login'),
...resultResolveTypeResolver('Signup'),
...resultResolveTypeResolver('SetLabels'),
...resultResolveTypeResolver('GenerateApiKey'),
}

View File

@ -20,6 +20,7 @@ export interface Claims {
uid: string
iat: number
userRole?: string
scope?: string // scope is used for api key like page:search
}
export type ClaimsToSet = {

View File

@ -1379,6 +1379,20 @@ const schema = gql`
NOT_FOUND
}
union GenerateApiKeyResult = GenerateApiKeySuccess | GenerateApiKeyError
type GenerateApiKeySuccess {
apiKey: String!
}
type GenerateApiKeyError {
errorCodes: [GenerateApiKeyErrorCode!]!
}
enum GenerateApiKeyErrorCode {
BAD_REQUEST
}
# Mutations
type Mutation {
googleLogin(input: GoogleLoginInput!): LoginResult!
@ -1439,6 +1453,7 @@ const schema = gql`
login(input: LoginInput!): LoginResult!
signup(input: SignupInput!): SignupResult!
setLabels(input: SetLabelsInput!): SetLabelsResult!
generateApiKey(scope: String): GenerateApiKeyResult!
}
# FIXME: remove sort from feedArticles after all cahced tabs are closed

View File

@ -1,4 +1,7 @@
import * as bcrypt from 'bcryptjs'
import * as jwt from 'jsonwebtoken'
import { env } from '../env'
import { Claims } from '../resolvers/types'
export const hashPassword = (password: string) => {
return bcrypt.hashSync(password, 10)
@ -7,3 +10,7 @@ export const hashPassword = (password: string) => {
export const comparePassword = (password: string, hash: string) => {
return bcrypt.compareSync(password, hash)
}
export const generateApiKey = (claims: Claims): string => {
return jwt.sign(claims, env.server.jwtSecret)
}

View File

@ -0,0 +1,47 @@
import { User } from '../../src/entity/user'
import { createTestUser, deleteTestUser } from '../db'
import { graphqlRequest, request } from '../util'
import { expect } from 'chai'
describe('generate api key', () => {
const username = 'fake_user'
let authToken: string
let user: User
before(async () => {
// create test user and login
user = await createTestUser(username)
const res = await request
.post('/local/debug/fake-user-login')
.send({ fakeEmail: user.email })
authToken = res.body.authToken
})
after(async () => {
// clean up
await deleteTestUser(username)
})
it('should return api key which could be used to make api calls', async () => {
const query = `
mutation {
generateApiKey {
... on GenerateApiKeySuccess {
apiKey
}
... on GenerateApiKeyError {
errorCodes
}
}
}
`
const response = await graphqlRequest(query, authToken).expect(200)
expect(response.body.data.generateApiKey.apiKey).to.be.a('string')
const apiKey = response.body.data.generateApiKey.apiKey
return graphqlRequest(query, apiKey).expect(200)
})
})