diff --git a/backend/Dockerfile b/backend/Dockerfile index 85b7204fe..e4d283a77 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -4,7 +4,10 @@ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --only-production --ignore-scripts +# RUN npm ci --only-production --ignore-scripts +# "prepare": "cd .. && npm install" + +RUN npm ci --only-production COPY . . diff --git a/backend/package.json b/backend/package.json index 174e7add5..0bcb36e49 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,7 +37,6 @@ "version": "1.0.0", "main": "src/index.js", "scripts": { - "prepare": "cd .. && npm install", "start": "npm run build && node build/index.js", "dev": "nodemon", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build", diff --git a/backend/src/middleware/requireAPIKeyDataAuth.ts b/backend/src/middleware/requireAPIKeyDataAuth.ts new file mode 100644 index 000000000..8dafb5a9c --- /dev/null +++ b/backend/src/middleware/requireAPIKeyDataAuth.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express'; +import { APIKeyData } from '../models'; +import { validateMembership } from '../helpers/membership'; +import { AccountNotFoundError } from '../utils/errors'; + +type req = 'params' | 'body' | 'query'; + +const requireAPIKeyDataAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + + // req.user + + const apiKeyData = await APIKeyData.findById(req[location].apiKeyDataId); + + if (!apiKeyData) { + return next(AccountNotFoundError({message: 'Failed to locate API Key data'})); + } + + await validateMembership({ + userId: req.user._id.toString(), + workspaceId: apiKeyData?.workspace.toString(), + acceptedRoles, + acceptedStatuses + }); + + req.apiKeyData = '' // ?? + + next(); + } +} + +export default requireAPIKeyDataAuth; \ No newline at end of file diff --git a/backend/src/models/apiKey.ts b/backend/src/models/apiKeyData.ts similarity index 61% rename from backend/src/models/apiKey.ts rename to backend/src/models/apiKeyData.ts index 488b24694..8b064bf08 100644 --- a/backend/src/models/apiKey.ts +++ b/backend/src/models/apiKeyData.ts @@ -1,12 +1,12 @@ import { Schema, model, Types } from 'mongoose'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; -// TODO: add scopes - -export interface IAPIKey { +export interface IAPIKeyData { name: string; - workspace: string; - environment: string; + workspaces: { + workspace: Types.ObjectId, + environments: string[] + }[]; expiresAt: Date; prefix: string; apiKeyHash: string; @@ -15,19 +15,22 @@ export interface IAPIKey { tag: string; } -const apiKeySchema = new Schema( +const apiKeyDataSchema = new Schema( { name: { type: String, required: true }, - workspace: { - type: String - }, - environment: { - type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] - }, + workspaces: [{ + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + environments: [{ + type: String, + enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] + }] + }], expiresAt: { type: Date }, @@ -58,6 +61,6 @@ const apiKeySchema = new Schema( } ); -const APIKey = model('APIKey', apiKeySchema); +const APIKeyData = model('APIKeyData', apiKeyDataSchema); -export default APIKey; +export default APIKeyData; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 0c40f155d..fd9578523 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -14,7 +14,7 @@ import Token, { IToken } from './token'; import User, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; import Workspace, { IWorkspace } from './workspace'; -import APIKey, { IAPIKey } from './apiKey'; +import APIKeyData, { IAPIKeyData } from './apiKeyData'; export { BackupPrivateKey, @@ -49,6 +49,6 @@ export { IUserAction, Workspace, IWorkspace, - APIKey, - IAPIKey, + APIKeyData, + IAPIKeyData, }; diff --git a/backend/src/routes/apiKey.ts b/backend/src/routes/apiKey.ts index c5fc7dd5d..a40c852ac 100644 --- a/backend/src/routes/apiKey.ts +++ b/backend/src/routes/apiKey.ts @@ -4,16 +4,14 @@ import { requireAuth } from '../middleware'; import { - APIKey + APIKeyData } from '../models'; -import { body } from 'express-validator'; +import { param, body, query } from 'express-validator'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; -// import * as bcrypt from 'bcrypt'; -// const bcrypt = require('bcrypt'); import * as Sentry from '@sentry/node'; -// POST /api/v1/api-key +// TODO: middleware router.post( '/', requireAuth, @@ -25,7 +23,7 @@ router.post( body('tag'), body('expiresAt'), async (req, res) => { - let savedAPIKey; + let apiKey, apiKeyData; try { const { name, @@ -37,14 +35,13 @@ router.post( expiresAt } = req.body; - // api-key: 38 characters - // 6-char: prefix - // 32-char: remaining - const apiKey = crypto.randomBytes(19).toString('hex'); - const saltRounds = 10; // config? + // create 38-char API key with first 6-char being the prefix + apiKey = crypto.randomBytes(19).toString('hex'); + + const saltRounds = 10; // TODO: add as config envar const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); - savedAPIKey = await new APIKey({ + apiKeyData = await new APIKeyData({ name, workspace, environment, @@ -55,22 +52,72 @@ router.post( iv, tag }).save(); - - // 1. generate api key - // 2. hash api key with bcrypt - // 3. store hash and api key info in db - // 4. return api key } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); return res.status(400).send({ - message: 'xxx' + message: 'Failed to create workspace API Key' }); } return res.status(200).send({ - apiKey: savedAPIKey + apiKey, + apiKeyData + }); + } +); + +// TODO: middleware +router.get( + '/', + requireAuth, + query('workspaceId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { workspaceId } = req.query; + + apiKeyData = await APIKeyData.find({ + workspace: workspaceId + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to get workspace API Key data' + }); + } + + return res.status(200).send({ + apiKeyData + }); + } +); + +// TODO: middleware +router.delete( + ':apiKeyDataId', + requireAuth, + // TODO: requireAPIKeyDataAuth, + param('apiKeyDataId').exists().trim(), + async (req, res) => { + let apiKeyData; + try { + const { apiKeyDataId } = req.params; + + apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); + + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: 'Failed to delete API key data' + }); + } + + return res.status(200).send({ + apiKeyData }); } );