Continue work on API key

This commit is contained in:
Tuan Dang
2022-12-25 19:19:56 -05:00
parent d869968f88
commit 888d28d6b9
6 changed files with 131 additions and 39 deletions
+4 -1
View File
@@ -4,7 +4,10 @@ WORKDIR /app
COPY package.json package-lock.json ./ 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 . . COPY . .
-1
View File
@@ -37,7 +37,6 @@
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"prepare": "cd .. && npm install",
"start": "npm run build && node build/index.js", "start": "npm run build && node build/index.js",
"dev": "nodemon", "dev": "nodemon",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
@@ -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;
@@ -1,12 +1,12 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables'; import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: add scopes export interface IAPIKeyData {
export interface IAPIKey {
name: string; name: string;
workspace: string; workspaces: {
environment: string; workspace: Types.ObjectId,
environments: string[]
}[];
expiresAt: Date; expiresAt: Date;
prefix: string; prefix: string;
apiKeyHash: string; apiKeyHash: string;
@@ -15,19 +15,22 @@ export interface IAPIKey {
tag: string; tag: string;
} }
const apiKeySchema = new Schema<IAPIKey>( const apiKeyDataSchema = new Schema<IAPIKeyData>(
{ {
name: { name: {
type: String, type: String,
required: true required: true
}, },
workspace: { workspaces: [{
type: String workspace: {
}, type: Schema.Types.ObjectId,
environment: { ref: 'Workspace'
type: String, },
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD] environments: [{
}, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]
}]
}],
expiresAt: { expiresAt: {
type: Date type: Date
}, },
@@ -58,6 +61,6 @@ const apiKeySchema = new Schema<IAPIKey>(
} }
); );
const APIKey = model<IAPIKey>('APIKey', apiKeySchema); const APIKeyData = model<IAPIKeyData>('APIKeyData', apiKeyDataSchema);
export default APIKey; export default APIKeyData;
+3 -3
View File
@@ -14,7 +14,7 @@ import Token, { IToken } from './token';
import User, { IUser } from './user'; import User, { IUser } from './user';
import UserAction, { IUserAction } from './userAction'; import UserAction, { IUserAction } from './userAction';
import Workspace, { IWorkspace } from './workspace'; import Workspace, { IWorkspace } from './workspace';
import APIKey, { IAPIKey } from './apiKey'; import APIKeyData, { IAPIKeyData } from './apiKeyData';
export { export {
BackupPrivateKey, BackupPrivateKey,
@@ -49,6 +49,6 @@ export {
IUserAction, IUserAction,
Workspace, Workspace,
IWorkspace, IWorkspace,
APIKey, APIKeyData,
IAPIKey, IAPIKeyData,
}; };
+66 -19
View File
@@ -4,16 +4,14 @@ import {
requireAuth requireAuth
} from '../middleware'; } from '../middleware';
import { import {
APIKey APIKeyData
} from '../models'; } from '../models';
import { body } from 'express-validator'; import { param, body, query } from 'express-validator';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
// import * as bcrypt from 'bcrypt';
// const bcrypt = require('bcrypt');
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
// POST /api/v1/api-key // TODO: middleware
router.post( router.post(
'/', '/',
requireAuth, requireAuth,
@@ -25,7 +23,7 @@ router.post(
body('tag'), body('tag'),
body('expiresAt'), body('expiresAt'),
async (req, res) => { async (req, res) => {
let savedAPIKey; let apiKey, apiKeyData;
try { try {
const { const {
name, name,
@@ -37,14 +35,13 @@ router.post(
expiresAt expiresAt
} = req.body; } = req.body;
// api-key: 38 characters // create 38-char API key with first 6-char being the prefix
// 6-char: prefix apiKey = crypto.randomBytes(19).toString('hex');
// 32-char: remaining
const apiKey = crypto.randomBytes(19).toString('hex'); const saltRounds = 10; // TODO: add as config envar
const saltRounds = 10; // config?
const apiKeyHash = await bcrypt.hash(apiKey, saltRounds); const apiKeyHash = await bcrypt.hash(apiKey, saltRounds);
savedAPIKey = await new APIKey({ apiKeyData = await new APIKeyData({
name, name,
workspace, workspace,
environment, environment,
@@ -55,22 +52,72 @@ router.post(
iv, iv,
tag tag
}).save(); }).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) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'xxx' message: 'Failed to create workspace API Key'
}); });
} }
return res.status(200).send({ 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
}); });
} }
); );