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:
@ -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>;
|
||||
|
||||
@ -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!
|
||||
|
||||
40
packages/api/src/resolvers/api_key/index.ts
Normal file
40
packages/api/src/resolvers/api_key/index.ts
Normal 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] }
|
||||
}
|
||||
})
|
||||
@ -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'),
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
47
packages/api/test/resolvers/api_key.test.ts
Normal file
47
packages/api/test/resolvers/api_key.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user