diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index f6b93caa6..763a72ba0 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -570,12 +570,15 @@ export type GenerateApiKeyError = { }; export enum GenerateApiKeyErrorCode { - BadRequest = 'BAD_REQUEST' + AlreadyExists = 'ALREADY_EXISTS', + BadRequest = 'BAD_REQUEST', + Unauthorized = 'UNAUTHORIZED' } export type GenerateApiKeyInput = { - expiredAt?: InputMaybe; - scope?: InputMaybe; + expiresAt: Scalars['Date']; + name: Scalars['String']; + scopes?: InputMaybe>; }; export type GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 3e228c912..88e0d0e12 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -500,12 +500,15 @@ type GenerateApiKeyError { } enum GenerateApiKeyErrorCode { + ALREADY_EXISTS BAD_REQUEST + UNAUTHORIZED } input GenerateApiKeyInput { - expiredAt: Date - scope: String + expiresAt: Date! + name: String! + scopes: [String!] } union GenerateApiKeyResult = GenerateApiKeyError | GenerateApiKeySuccess diff --git a/packages/api/src/resolvers/api_key/index.ts b/packages/api/src/resolvers/api_key/index.ts index 538a7c837..078076b16 100644 --- a/packages/api/src/resolvers/api_key/index.ts +++ b/packages/api/src/resolvers/api_key/index.ts @@ -4,33 +4,53 @@ import { GenerateApiKeySuccess, MutationGenerateApiKeyArgs, } from '../../generated/graphql' -import { generateApiKey } from '../../utils/auth' import { analytics } from '../../utils/analytics' import { env } from '../../env' import { authorized } from '../../utils/helpers' +import { getRepository } from '../../entity/utils' +import { User } from '../../entity/user' +import { ApiKey } from '../../entity/api_key' +import { generateApiKey, hashKey } from '../../utils/auth' export const generateApiKeyResolver = authorized< GenerateApiKeySuccess, GenerateApiKeyError, MutationGenerateApiKeyArgs ->((_, { input: { scope, expiredAt } }, { claims }) => { +>(async (_, { input: { name, expiresAt } }, { claims: { uid }, log }) => { try { - console.log('generateApiKeyResolver', scope, expiredAt) + log.info('generateApiKeyResolver') + const user = await getRepository(User).findOneBy({ id: uid }) + if (!user) { + return { + errorCodes: [GenerateApiKeyErrorCode.Unauthorized], + } + } - const exp = expiredAt ? new Date(expiredAt).getTime() / 1000 : null - const apiKey = generateApiKey({ - iat: new Date().getTime(), - scope: scope || 'all', - uid: claims.uid, - ...(exp && { exp }), + const existingApiKey = await getRepository(ApiKey).findOneBy({ + user: { id: uid }, + name, + }) + if (existingApiKey) { + return { + errorCodes: [GenerateApiKeyErrorCode.AlreadyExists], + } + } + + const exp = new Date(expiresAt) + const apiKey = generateApiKey() + await getRepository(ApiKey).save({ + user: { id: uid }, + name, + key: hashKey(apiKey), + expiresAt: exp, }) analytics.track({ - userId: claims.uid, - event: 'generate_api_key', + userId: uid, + event: 'api_key_generated', properties: { - scope, - expiredAt: exp, + name, + expiresAt: exp, env: env.server.apiEnv, }, }) @@ -38,6 +58,7 @@ export const generateApiKeyResolver = authorized< return { apiKey } } catch (error) { console.error(error) + return { errorCodes: [GenerateApiKeyErrorCode.BadRequest] } } }) diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index 6cd7bce63..d6c16092a 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -33,7 +33,7 @@ import { env } from '../../env' import { validateUsername } from '../../utils/usernamePolicy' import * as jwt from 'jsonwebtoken' import { createUser } from '../../services/create_user' -import { comparePassword, hashPassword } from '../../utils/auth' +import { compareHashedKey, hashKey } from '../../utils/auth' export const updateUserResolver = authorized< UpdateUserSuccess, @@ -310,7 +310,7 @@ export const loginResolver: ResolverFn< } // check if password is correct - const validPassword = comparePassword(password, user.password) + const validPassword = compareHashedKey(password, user.password) if (!validPassword) { return { errorCodes: [LoginErrorCode.InvalidCredentials] } } @@ -331,7 +331,7 @@ export const signupResolver: ResolverFn< try { // hash password - const hashedPassword = hashPassword(password) + const hashedPassword = hashKey(password) const [user, profile] = await createUser({ email, diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index 5b634277e..717c57c52 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -1412,8 +1412,9 @@ const schema = gql` } input GenerateApiKeyInput { - scope: String - expiredAt: Date + name: String! + scopes: [String!] + expiresAt: Date! } union GenerateApiKeyResult = GenerateApiKeySuccess | GenerateApiKeyError @@ -1428,6 +1429,8 @@ const schema = gql` enum GenerateApiKeyErrorCode { BAD_REQUEST + ALREADY_EXISTS + UNAUTHORIZED } # Query: search diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts index e86eb80eb..a6814fb8b 100644 --- a/packages/api/src/utils/auth.ts +++ b/packages/api/src/utils/auth.ts @@ -1,16 +1,15 @@ import * as bcrypt from 'bcryptjs' -import * as jwt from 'jsonwebtoken' -import { env } from '../env' -import { Claims } from '../resolvers/types' +import { v4 as uuidv4 } from 'uuid' -export const hashPassword = (password: string) => { - return bcrypt.hashSync(password, 10) +export const hashKey = (key: string, salt = 10) => { + return bcrypt.hashSync(key, salt) } -export const comparePassword = (password: string, hash: string) => { - return bcrypt.compareSync(password, hash) +export const compareHashedKey = (rawKey: string, hash: string) => { + return bcrypt.compareSync(rawKey, hash) } -export const generateApiKey = (claims: Claims): string => { - return jwt.sign(claims, env.server.jwtSecret) +export const generateApiKey = (): string => { + // TODO: generate random string key + return uuidv4() } diff --git a/packages/api/test/gql/sanitize-directive.test.ts b/packages/api/test/gql/sanitize-directive.test.ts index 0c81566ec..f60bfcab6 100644 --- a/packages/api/test/gql/sanitize-directive.test.ts +++ b/packages/api/test/gql/sanitize-directive.test.ts @@ -1,7 +1,7 @@ import { createTestUser, deleteTestUser } from '../db' import { graphqlRequest, request } from '../util' import { User } from '../../src/entity/user' -import { hashPassword } from '../../src/utils/auth' +import { hashKey } from '../../src/utils/auth' import 'mocha' describe('Sanitize Directive', () => { @@ -12,7 +12,7 @@ describe('Sanitize Directive', () => { let user: User before(async () => { - const hashedPassword = hashPassword(correctPassword) + const hashedPassword = hashKey(correctPassword) user = await createTestUser(username, '', hashedPassword) const res = await request .post('/local/debug/fake-user-login') diff --git a/packages/api/test/resolvers/user.test.ts b/packages/api/test/resolvers/user.test.ts index 07d49c851..cc065f6be 100644 --- a/packages/api/test/resolvers/user.test.ts +++ b/packages/api/test/resolvers/user.test.ts @@ -8,7 +8,7 @@ import { UpdateUserProfileErrorCode, } from '../../src/generated/graphql' import { User } from '../../src/entity/user' -import { hashPassword } from '../../src/utils/auth' +import { hashKey } from '../../src/utils/auth' import 'mocha' describe('User API', () => { @@ -21,7 +21,7 @@ describe('User API', () => { let anotherUser: User before(async () => { - const hashedPassword = hashPassword(correctPassword) + const hashedPassword = hashKey(correctPassword) // create test user and login user = await createTestUser(username, '', hashedPassword) const res = await request diff --git a/packages/db/migrations/0084.do.api_key.sql b/packages/db/migrations/0084.do.api_key.sql index 6a9cf296b..b258a9685 100755 --- a/packages/db/migrations/0084.do.api_key.sql +++ b/packages/db/migrations/0084.do.api_key.sql @@ -12,7 +12,8 @@ CREATE TABLE omnivore.api_key ( scopes text[] NOT NULL DEFAULT '{}', expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT current_timestamp, - used_at timestamptz + used_at timestamptz, + UNIQUE (user_id, name) ); COMMIT;