diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts new file mode 100644 index 000000000..e09504d49 --- /dev/null +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -0,0 +1,54 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import { + APIKeyData +} from '../../models'; +import { + SALT_ROUNDS +} from '../../config'; + +/** + * Create new API key for user with id [req.user._id] + * @param req + * @param res + */ +export const createAPIKey = async (req: Request, res: Response) => { + let apiKey, apiKeyData; + try { + const { name, expiresIn } = req.body; + + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, SALT_ROUNDS); + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + apiKeyData = await new APIKeyData({ + name, + expiresAt, + user: req.user._id, + secretHash + }); + + // return api key data without sensitive data + apiKeyData = await APIKeyData.findById(apiKeyData._id); + + if (!apiKeyData) throw new Error('Failed to find API key data'); + + apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to create service token data' + }); + } + + return res.status(200).send({ + apiKey, + apiKeyData + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index d4729c15c..e867f7692 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,7 +1,9 @@ import * as workspaceController from './workspaceController'; import * as serviceTokenDataController from './serviceTokenDataController'; +import * as apiKeyDataController from './apiKeyDataController'; export { workspaceController, - serviceTokenDataController + serviceTokenDataController, + apiKeyDataController } diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts new file mode 100644 index 000000000..af73b5f69 --- /dev/null +++ b/backend/src/models/apiKeyData.ts @@ -0,0 +1,37 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IAPIKeyData { + name: string; + user: Types.ObjectId; + expiresAt: Date; + secretHash: string; +} + +const apiKeyDataSchema = new Schema( + { + name: { + type: String, + required: true + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + expiresAt: { + type: Date + }, + secretHash: { + type: String, + required: true, + select: false + } + }, + { + timestamps: true + } +); + +const APIKeyData = model('APIKeyData', apiKeyDataSchema); + +export default APIKeyData; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 8e934d511..72ffca607 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,8 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import ServiceTokenData, { IServiceTokenData } from './serviceTokenData '; +import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; +import APIKeyData, { IAPIKeyData } from './apiKeyData'; export { BackupPrivateKey, @@ -50,5 +51,7 @@ export { Workspace, IWorkspace, ServiceTokenData, - IServiceTokenData + IServiceTokenData, + APIKeyData, + IAPIKeyData }; diff --git a/backend/src/models/serviceTokenData .ts b/backend/src/models/serviceTokenData.ts similarity index 93% rename from backend/src/models/serviceTokenData .ts rename to backend/src/models/serviceTokenData.ts index 8e8ae5eab..612d07bf6 100644 --- a/backend/src/models/serviceTokenData .ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,5 +1,4 @@ import { Schema, model, Types } from 'mongoose'; -import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; export interface IServiceTokenData { name: string; @@ -38,7 +37,6 @@ const serviceTokenDataSchema = new Schema( }, secretHash: { type: String, - unique: true, required: true, select: false }, diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts new file mode 100644 index 000000000..c6a6a1761 --- /dev/null +++ b/backend/src/routes/v2/apiKeyData.ts @@ -0,0 +1,21 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + validateRequest +} from '../../middleware'; +import { body } from 'express-validator'; +import { apiKeyDataController } from '../../controllers/v2'; + +router.post( + '/', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + body('name').exists().trim(), + body('expiresIn'), // measured in ms + validateRequest, + apiKeyDataController.createAPIKey +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index acf115a92..d0f3833ba 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,9 +1,11 @@ import secret from './secret'; import workspace from './workspace'; import serviceTokenData from './serviceTokenData'; +import apiKeyData from './apiKeyData'; export { secret, workspace, - serviceTokenData + serviceTokenData, + apiKeyData }