Merge pull request #3 from ossiggy/mo/server-refactor

Mo/server refactor
This commit is contained in:
Michael Ossig
2022-03-18 15:38:43 -04:00
committed by GitHub
35 changed files with 429 additions and 88 deletions

104
package-lock.json generated
View File

@@ -3640,6 +3640,12 @@
"@babel/types": "^7.3.0"
}
},
"node_modules/@types/bcryptjs": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
"integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==",
"dev": true
},
"node_modules/@types/bluebird": {
"version": "3.5.36",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz",
@@ -3822,6 +3828,15 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"node_modules/@types/jsonwebtoken": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz",
"integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -3873,6 +3888,38 @@
"@types/express": "*"
}
},
"node_modules/@types/passport-jwt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
"integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/jsonwebtoken": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-local": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz",
"integrity": "sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
"integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/prettier": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz",
@@ -25627,13 +25674,18 @@
"validator": "^13.7.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/chai": "^4.3.0",
"@types/cookie-parser": "^1.4.2",
"@types/expect": "^24.3.0",
"@types/express": "^4.17.13",
"@types/express-serve-static-core": "^4.17.28",
"@types/jsonwebtoken": "^8.5.8",
"@types/mocha": "^9.1.0",
"@types/mongoose": "^5.11.97",
"@types/passport": "^1.0.7",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"@types/request-promise": "^4.1.48",
"chai": "^4.3.6",
"chai-http": "^4.3.0",
@@ -28124,6 +28176,12 @@
"@babel/types": "^7.3.0"
}
},
"@types/bcryptjs": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz",
"integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==",
"dev": true
},
"@types/bluebird": {
"version": "3.5.36",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz",
@@ -28305,6 +28363,15 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
},
"@types/jsonwebtoken": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.8.tgz",
"integrity": "sha512-zm6xBQpFDIDM6o9r6HSgDeIcLy82TKWctCXEPbJJcXb5AKmi5BNNdLXneixK4lplX3PqIVcwLBCGE/kAGnlD4A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -28355,6 +28422,38 @@
"@types/express": "*"
}
},
"@types/passport-jwt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-3.0.6.tgz",
"integrity": "sha512-cmAAMIRTaEwpqxlrZyiEY9kdibk94gP5KTF8AT1Ra4rWNZYHNMreqhKUEeC5WJtuN5SJZjPQmV+XO2P5PlnvNQ==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/jsonwebtoken": "*",
"@types/passport-strategy": "*"
}
},
"@types/passport-local": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.34.tgz",
"integrity": "sha512-PSc07UdYx+jhadySxxIYWuv6sAnY5e+gesn/5lkPKfBeGuIYn9OPR+AAEDq73VRUh6NBTpvE/iPE62rzZUslog==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"@types/passport-strategy": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
"integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*"
}
},
"@types/prettier": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.4.tgz",
@@ -28981,13 +29080,18 @@
"adyen-demo-server": {
"version": "file:packages/server",
"requires": {
"@types/bcryptjs": "*",
"@types/chai": "^4.3.0",
"@types/cookie-parser": "^1.4.2",
"@types/expect": "^24.3.0",
"@types/express": "^4.17.13",
"@types/express-serve-static-core": "^4.17.28",
"@types/jsonwebtoken": "^8.5.8",
"@types/mocha": "^9.1.0",
"@types/mongoose": "^5.11.97",
"@types/passport": "^1.0.7",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"@types/request-promise": "^4.1.48",
"@types/validator": "^13.7.1",
"bcrypt": "^5.0.1",

View File

@@ -1,2 +1 @@
export { updateUserInfo, clearUserInfo } from './reducers/user';
export { updateConfigurationInfo, clearConfigurationInfo } from './reducers/configuration';
export { userActions, configurationActions } from './reducers';

View File

@@ -1,5 +1,5 @@
import * as actions from './actions';
import * as selectors from './selectors';
import reducers from './reducers';
export * as actions from './actions';
export * as selectors from './selectors';
export { userReducer, configurationReducer } from './reducers';
export { actions, selectors, reducers };
export type { ConfigurationState, UserState } from './types';

View File

@@ -23,6 +23,4 @@ export const configurationSlice = createSlice({
}
});
export const { updateConfigurationInfo, clearConfigurationInfo } = configurationSlice.actions;
export default configurationSlice.reducer;
export const { actions, reducer } = configurationSlice;

View File

@@ -1,4 +1,2 @@
import userReducer from './user';
import configurationReducer from './configuration';
export default { userReducer, configurationReducer };
export { reducer as userReducer, actions as userActions } from './user';
export { reducer as configurationReducer, actions as configurationActions } from './configuration';

View File

@@ -20,6 +20,4 @@ export const userSlice = createSlice({
}
});
export const { updateUserInfo, clearUserInfo } = userSlice.actions;
export default userSlice.reducer;
export const { actions, reducer } = userSlice;

View File

@@ -1,9 +1,10 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { useCheckout } from './checkout/useCheckout';
import { useInitializeCheckout } from './checkout/useInitializeCheckout';
import type { RootState, AppDispatch } from '../store';
const useAppDispatch = () => useDispatch<AppDispatch>();
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export type { InitializationRequest, EditableCheckoutConfigFields, CheckoutConfig, PaymentAmount, PaymentMethodsResponseInterface } from './types';
export { useCheckout, useInitializeCheckout, useAppDispatch, useAppSelector };
export { useCheckout } from './checkout/useCheckout';
export { useInitializeCheckout } from './checkout/useInitializeCheckout';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,4 +1,4 @@
import { PaymentAmount, PaymentMethodsResponseInterface } from '@adyen/adyen-web/dist/types/types';
import type { PaymentAmount, PaymentMethodsResponseInterface } from '@adyen/adyen-web/dist/types/types';
export type InitializationRequest = {
merchantAccount: string;
@@ -39,3 +39,5 @@ export interface CheckoutConfig extends EditableCheckoutConfigFields {
onError?: (error: any, element?: any) => void;
onPaymentCompleted?: (result: any, element: any) => void;
}
export { PaymentAmount, PaymentMethodsResponseInterface };

View File

@@ -1,5 +1,5 @@
export const PORT = process.env.PORT || 8080;
export const ADYEN_API_KEY = process.env.ADYEN_API_KEY || null;
export const ADYEN_API_KEY = process.env.ADYEN_API_KEY || '';
export const ADYEN_BASE_URL = process.env.ADYEN_BASE_URL || 'https://checkout-test.adyen.com';
export const DATABASE_URL = process.env.DATABASE_URL || 'mongodb://localhost/adyen-demo';
export const TEST_DATABASE_URL = process.env.TEST_DATABASE_URL || 'mongodb://localhost/adyen-demo-test';

View File

@@ -7,7 +7,7 @@ export const mongoOptions = {
useUnifiedTopology: true
};
export const dbConnect = url => {
export const dbConnect = (url: string) => {
return mongoose.connect(url, mongoOptions as ConnectOptions).catch(err => {
console.error('Mongoose failed to connect');
console.error(err);

View File

@@ -34,7 +34,7 @@ app.use('/sessions', sessionsRouter);
app.use('/payments', paymentsRouter);
app.use('/configurations', configurationRouter);
let server;
let server: any;
export const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
return new Promise<void>((resolve, reject) => {
@@ -59,7 +59,7 @@ export const closeServer = () => {
return mongoose.disconnect().then(() => {
return new Promise<void>((resolve, reject) => {
console.log('Closing server');
return server.close(err => {
return server.close((err: any) => {
if (err) {
return reject(err);
}

View File

@@ -1 +1,2 @@
export { User, Configuration } from './users';
export type { UserDocument, ConfigurationDocument } from './types';

View File

@@ -7,8 +7,8 @@ export interface UserDocument extends Document {
password: string;
email: string;
adyenKey?: string;
merchantAccounts?: [string];
configurations?: [Types.ObjectId];
merchantAccounts?: string[];
configurations?: Types.ObjectId[];
}
export interface ConfigurationDocument extends Document {

View File

@@ -1,4 +1,4 @@
import { Schema, SchemaTypes, model } from 'mongoose';
import { Schema, SchemaTypes, Model, model } from 'mongoose';
import { ConfigurationDocument } from '../types';
@@ -6,6 +6,8 @@ export interface Configuration extends ConfigurationDocument {
apiRepr(): ConfigurationDocument;
}
export interface ConfigurationModel extends Model<Configuration> {}
export const ConfigurationSchema: Schema = new Schema({
owner: { type: SchemaTypes.ObjectId, ref: 'User', required: true },
name: { type: String, required: true },
@@ -25,6 +27,6 @@ ConfigurationSchema.method('apiRepr', function () {
};
});
export const Configuration = model<Configuration>('Configuration', ConfigurationSchema);
export const Configuration: ConfigurationModel = model<Configuration, ConfigurationModel>('Configuration', ConfigurationSchema);
export default Configuration;

View File

@@ -50,11 +50,11 @@ UserSchema.method('apiRepr', function () {
};
});
UserSchema.method('validatePassword', function (password: string): boolean {
UserSchema.method('validatePassword', function (password: string): Promise<boolean> {
return compare(password, this.password);
});
UserSchema.static('hashPassword', (password: string): string => hash(password, 10));
UserSchema.static('hashPassword', (password: string): Promise<string> => hash(password, 10));
export const User: UserModel = model<User, UserModel>('User', UserSchema);

View File

@@ -28,13 +28,18 @@
"validator": "^13.7.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@types/chai": "^4.3.0",
"@types/cookie-parser": "^1.4.2",
"@types/expect": "^24.3.0",
"@types/express": "^4.17.13",
"@types/express-serve-static-core": "^4.17.28",
"@types/jsonwebtoken": "^8.5.8",
"@types/mocha": "^9.1.0",
"@types/mongoose": "^5.11.97",
"@types/passport": "^1.0.7",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"@types/request-promise": "^4.1.48",
"chai": "^4.3.6",
"chai-http": "^4.3.0",

View File

@@ -1,2 +1,4 @@
export { router as sessionsRouter } from './sessions';
export { router as paymentsRouter } from './payments';
export type { BaseAdyenRequest, InitializationRequest, RequestOptions, PaymentAmount } from './types';

View File

@@ -1,4 +1,4 @@
import { Router } from 'express';
import { Request, Response, Router } from 'express';
import request from 'request-promise';
import { errorHandler } from '../helpers';
import { ADYEN_API_KEY, ADYEN_BASE_URL } from '../../config';
@@ -8,7 +8,7 @@ import type { PaymentMethodsResponseInterface } from '@adyen/adyen-web/dist/type
const router = Router();
router.post('/getPaymentMethods', async (req, res) => {
router.post('/getPaymentMethods', async (req: Request, res: Response) => {
const { version, apiKey, payload }: InitializationRequest = req.body;
try {
const options: RequestOptions = {
@@ -23,12 +23,12 @@ router.post('/getPaymentMethods', async (req, res) => {
const response: PaymentMethodsResponseInterface = await request(options);
res.send(201).json(response);
} catch (err) {
} catch (err: any) {
errorHandler('/getPaymentMethods', 500, err.message, res);
}
});
router.post('/makePayment', async (req, res) => {
router.post('/makePayment', async (req: Request, res: Response) => {
const { version, apiKey, payload } = req.body;
try {
const options: RequestOptions = {
@@ -43,12 +43,12 @@ router.post('/makePayment', async (req, res) => {
const response = await request(options);
res.send(201).json(response);
} catch (err) {
} catch (err: any) {
errorHandler('/makePayment', 500, err.message, res);
}
});
router.post('/additionalDetails', async (req, res) => {
router.post('/additionalDetails', async (req: Request, res: Response) => {
const { version, apiKey, payload } = req.body;
try {
const options: RequestOptions = {
@@ -63,7 +63,7 @@ router.post('/additionalDetails', async (req, res) => {
const response = await request(options);
res.send(201).json(response);
} catch (err) {
} catch (err: any) {
errorHandler('/additionalDetails', 500, err.message, res);
}
});

View File

@@ -1,18 +1,18 @@
import { Router } from 'express';
import { Router, Request, Response } from 'express';
import request from 'request-promise';
import { errorHandler } from '../helpers';
import { ADYEN_API_KEY, ADYEN_BASE_URL } from '../../config';
import type { InitializationRequest } from './types';
import { CheckoutSessionSetupResponse } from '@adyen/adyen-web/dist/types/types';
import type { InitializationRequest, RequestOptions } from './types';
import type { CheckoutSessionSetupResponse } from '@adyen/adyen-web/dist/types/types';
const router = Router();
router.post('/sessionStart', async (req, res) => {
router.post('/sessionStart', async (req: Request, res: Response) => {
const { version, apiKey, payload }: InitializationRequest = req.body;
try {
const options = {
const options: RequestOptions = {
url: `${ADYEN_BASE_URL}/${version}/sessions`,
headers: {
'Content-type': 'application/json',
@@ -24,7 +24,7 @@ router.post('/sessionStart', async (req, res) => {
const response: CheckoutSessionSetupResponse = await request(options);
res.send(201).json(response);
} catch (err) {
} catch (err: any) {
errorHandler('/sessionStart', 500, err.message, res);
}
});

View File

@@ -29,3 +29,5 @@ export interface RequestOptions {
body: any;
json: boolean;
}
export type { PaymentAmount };

View File

@@ -1,11 +1,11 @@
import jwt from 'jsonwebtoken';
import express from 'express';
import passport from 'passport';
import express, { Request, Response } from 'express';
import { JWT_EXPIRY, JWT_SECRET } from '../../config';
const router = express.Router();
const createAuthToken = (user: any): object => {
const createAuthToken = (user: any): string => {
return jwt.sign({ user }, JWT_SECRET, {
subject: user.username,
expiresIn: JWT_EXPIRY,
@@ -15,14 +15,14 @@ const createAuthToken = (user: any): object => {
const localAuth = passport.authenticate('local', { session: false });
router.post('/login', localAuth, (req, res) => {
router.post('/login', localAuth, (req: Request, res: Response) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});
const jwtAuth = passport.authenticate('jwt', { session: false });
router.post('/refresh', jwtAuth, (req, res) => {
router.post('/refresh', jwtAuth, (req: Request, res: Response) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});

View File

@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary } from 'express-serve-static-core';
import jwt_decode from 'jwt-decode';
import { Types } from 'mongoose';
type TokenData = {
user: {
@@ -12,8 +13,16 @@ type TokenData = {
};
};
export const isAuthorizedForAction = (req, res, next) => {
const userToken: string = req.headers.authorization.split(' ')[1];
type AuthorizedParams = {
userId: string;
};
interface AuthorizationRequest<T extends ParamsDictionary> extends Request {
params: T;
}
export const isAuthorizedForAction = (req: AuthorizationRequest<AuthorizedParams>, res: Response, next: NextFunction) => {
const userToken: string = req.headers.authorization ? req.headers.authorization.split(' ')[1] : '';
const { userId }: { userId: string } = req.params;
const tokenData: TokenData = jwt_decode(userToken);
return tokenData.user._id === userId

View File

@@ -25,7 +25,7 @@ export const localStrategy = new LocalStrategy(async (username, password, callba
}
return callback(null, user);
} catch (err) {
} catch (err: any) {
if (err.reason === 'LoginError') {
return callback(null, false, err);
}

View File

@@ -1,11 +1,13 @@
import { Router } from 'express';
import { Router, Request, Response } from 'express';
import { jwtAuth, isAuthorizedForAction } from '../auth';
import { User, Configuration } from '../../models';
import { jwtAuth, isAuthorizedForAction } from '../auth';
import type { ConfigToUpdate } from './types';
const router = Router();
router.get('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
router.get('/:userId', jwtAuth, isAuthorizedForAction, async (req: Request, res: Response) => {
try {
const existingUser = await User.find({ _id: req.params.userId });
if (!existingUser || !existingUser.length) {
@@ -28,13 +30,13 @@ router.get('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
res.status(201).json(relatedConfigurations.map(config => config.apiRepr()));
} catch (err) {
} catch (err: any) {
console.error('ERROR GETTING CONFIGS', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
router.get('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req, res) => {
router.get('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req: Request, res: Response) => {
try {
const existingConfiguration = await Configuration.find({ _id: req.params.id });
if (!existingConfiguration || !existingConfiguration.length) {
@@ -53,7 +55,7 @@ router.get('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
});
router.post('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
router.post('/:userId', jwtAuth, isAuthorizedForAction, async (req: Request, res: Response) => {
try {
const { owner, name, version, configuration } = req.body;
@@ -75,15 +77,15 @@ router.post('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
});
router.put('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req, res) => {
router.put('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req: Request, res: Response) => {
if (!(req.params.id === req.body.id)) {
const message = `Request patch id (${req.params.id} and request body id (${req.body.id}) must match)`;
console.error(message);
res.status(400).json({ message: message });
}
try {
const toUpdate = {};
const updateableFields = ['name', 'version', 'configuration'];
const toUpdate: ConfigToUpdate = {};
const updateableFields = ['name' as const, 'version' as const, 'configuration' as const];
updateableFields.forEach(field => {
if (field in req.body) {
@@ -91,12 +93,12 @@ router.put('/:userId/:id', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
});
const { owner, name, version, configuration } = await Configuration.findOneAndUpdate(
{ _id: req.body.id },
{ $set: toUpdate },
{ new: true }
).exec();
res.send(200).json({ id: req.body.id, owner, name, version, configuration });
const updatedConfig = await Configuration.findOneAndUpdate({ _id: req.body.id }, { $set: toUpdate }, { new: true }).exec();
if (updatedConfig) {
const { owner, name, version, configuration } = updatedConfig;
return res.send(200).json({ id: req.body.id, owner, name, version, configuration });
}
res.status(500).json({ message: 'Internal server error' });
} catch (err) {
console.error('CONFIGURATIONS UPDATE ERROR', err);
res.status(500).json({ message: 'Internal server error' });

View File

@@ -0,0 +1,11 @@
export type ConfigToUpdate = {
name?: string;
version?: number;
configuration?: string;
};
export type UserToUpdate = {
adyenKey?: string;
merchantAccounts?: string[];
configurations?: string[];
};

View File

@@ -1,10 +1,13 @@
import { Router } from 'express';
import { jwtAuth, isAuthorizedForAction } from '../auth';
import { runUserValidation } from '../helpers';
const router = Router();
import { User } from '../../models';
import { runUserValidation } from '../helpers';
import { jwtAuth, isAuthorizedForAction } from '../auth';
import type { UserToUpdate } from './types';
const router = Router();
router.post('/', async (req, res) => {
try {
@@ -69,8 +72,8 @@ router.put('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
try {
const toUpdate = {};
const updateableFields = ['adyenKey', 'merchantAccounts', 'configurations'];
const toUpdate: UserToUpdate = {};
const updateableFields = ['adyenKey' as const, 'merchantAccounts' as const, 'configurations' as const];
updateableFields.forEach(field => {
if (field in req.body) {
@@ -78,13 +81,12 @@ router.put('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
}
});
const { adyenKey, merchantAccounts, configurations } = await User.findOneAndUpdate(
{ _id: req.body.id },
{ $set: toUpdate },
{ new: true }
).exec();
const foundUser = await User.findOneAndUpdate({ _id: req.body.id }, { $set: toUpdate }, { new: true }).exec();
res.status(200).json({ id: req.body.id, adyenKey: adyenKey.substr(adyenKey.length - 5), merchantAccounts, configurations });
if (foundUser) {
const { adyenKey, merchantAccounts, configurations } = foundUser;
res.status(200).json({ id: req.body.id, adyenKey: adyenKey ? adyenKey.substring(adyenKey.length - 5) : '', merchantAccounts, configurations });
}
} catch (err) {
console.error('USER UPDATE ERROR', err);
res.status(500).json({ message: 'Internal server error' });

View File

@@ -1,10 +1,12 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import { app } from '../../../index';
const { testUserData, testConfigData } = require('../../structures').userTestData;
import { app } from '../../index';
import { userTestData } from '../structures';
chai.use(chaiHttp);
const { testUserData, testConfigData } = userTestData;
export const createMockUser = (): any => {
return chai
.request(app)

View File

@@ -0,0 +1 @@
export * as userHelpers from './helpers';

View File

@@ -0,0 +1,197 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import jwt from 'jsonwebtoken';
import { User } from '../../../models';
import { userTestData } from '../../structures';
import { JWT_SECRET, TEST_DATABASE_URL } from '../../../config';
import { app, runServer, closeServer } from '../../../index';
const assert = chai.assert;
chai.use(chaiHttp);
const {
testUserData: { username, password, email }
} = userTestData;
describe('Authorization API', () => {
let _id: string;
before(() => {
return runServer(TEST_DATABASE_URL);
});
after(() => {
return closeServer();
});
beforeEach(async () => {
const hashedPassword = await User.hashPassword(password);
if (hashedPassword) {
const { id } = await User.create({
username,
password: hashedPassword,
email
});
if (id) {
_id = id;
}
}
});
afterEach(() => {
return User.deleteOne({ _id });
});
describe('/login', () => {
it('Should reject requests with no credentials', async () => {
try {
return await chai.request(app).post('/auth/login');
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 400);
}
});
it('Should reject requests with incorrect usernames', async () => {
try {
return await chai.request(app).post('/auth/login').auth('wrongUsername', password);
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 400);
}
});
it('Should reject requests with incorrect passwords', async () => {
try {
return await chai.request(app).post('/auth/login').auth(username, 'wrongPassword');
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 400);
}
});
it('Should return a valid auth token', async () => {
const res = await chai.request(app).post('/auth/login').send({ username, password });
assert.equal(res.status, 200);
assert.equal(typeof res.body, 'object');
const token = res.body.authToken;
assert.equal(typeof token, 'string');
const payload: any = jwt.verify(token, JWT_SECRET, {
algorithms: ['HS256']
});
assert.equal(payload.user.username, username, 'failed username match');
assert.equal(payload.user._id, _id, 'failed id match');
});
});
describe('/auth/refresh', () => {
it('Should reject requests with no credentials', async () => {
try {
return await chai.request(app).post('/auth/refresh');
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 401);
}
});
it('Should reject requests with an invalid token', async () => {
const token = jwt.sign(
{
username,
email
},
'wrongSecret',
{
algorithm: 'HS256',
expiresIn: '7d'
}
);
try {
return await chai.request(app).post('/auth/refresh').set('Authorization', `Bearer ${token}`);
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 401);
}
});
it('Should reject requests with an expired token', async () => {
const token = jwt.sign(
{
user: {
username,
email
},
exp: Math.floor(Date.now() / 1000) - 10 // Expired ten seconds ago
},
JWT_SECRET,
{
algorithm: 'HS256',
subject: username
}
);
try {
return await chai.request(app).post('/auth/refresh').set('authorization', `Bearer ${token}`);
} catch (err: any) {
if (err instanceof chai.AssertionError) {
throw err;
}
const res = err.response;
assert.equal(res.status, 401);
}
});
it('Should return a valid auth token with a newer expiry date', async () => {
const token = jwt.sign(
{
user: {
username,
email
}
},
JWT_SECRET,
{
algorithm: 'HS256',
subject: username,
expiresIn: '7d'
}
);
const decoded: any = jwt.decode(token);
const res = await chai.request(app).post('/auth/refresh').set('authorization', `Bearer ${token}`);
assert.equal(res.status, 200);
assert.equal(typeof res.body, 'object');
const token_2 = res.body.authToken;
assert.equal(typeof token_2, 'string');
const payload: any = jwt.verify(token_2, JWT_SECRET, {
algorithms: ['HS256']
});
assert.deepEqual(payload.user, { username, email });
assert.isAtLeast(decoded.exp, payload.exp);
});
});
});

View File

@@ -1,15 +1,17 @@
import chai from 'chai';
import mongoose from 'mongoose';
import chaiHttp from 'chai-http';
const { wrongAuthToken } = require('../../structures').userTestData;
import { userTestData } from '../../structures';
import { TEST_DATABASE_URL } from '../../../config';
import { createMockConfigurations } from './helpers';
import { userHelpers } from '../../helpers';
import { app, runServer, closeServer } from '../../../index';
const assert = chai.assert;
chai.use(chaiHttp);
const { createMockConfigurations } = userHelpers;
const tearDownDb = () => {
return new Promise((resolve, reject) => {
mongoose.connection
@@ -46,7 +48,7 @@ describe('Configurations API', () => {
let agent = chai.request.agent(app);
return agent
.get(`/configurations/${userId}/${mockConfig.body.id}`)
.set('Authorization', `Bearer ${wrongAuthToken}`)
.set('Authorization', `Bearer ${userTestData.wrongAuthToken}`)
.then(res => {
assert.equal(res.status, 401, 'failed status check');
return res;

View File

@@ -1,15 +1,17 @@
import chai from 'chai';
import mongoose from 'mongoose';
import chaiHttp from 'chai-http';
const { wrongAuthToken } = require('../../structures').userTestData;
import { userTestData } from '../../structures';
import { TEST_DATABASE_URL } from '../../../config';
import { createMockUser, logUserIn } from './helpers';
import { userHelpers } from '../../helpers';
import { app, runServer, closeServer } from '../../../index';
const assert = chai.assert;
chai.use(chaiHttp);
const { createMockUser, logUserIn } = userHelpers;
const tearDownDb = () => {
return new Promise((resolve, reject) => {
mongoose.connection
@@ -63,7 +65,7 @@ describe('Users API', () => {
const authToken = await logUserIn();
const mockPayload = {
id: mockUser.body.id,
adyenKey: wrongAuthToken,
adyenKey: userTestData.wrongAuthToken,
merchantAccounts: ['TestMerchant1', 'TestMerchant2']
};

View File

@@ -4,6 +4,7 @@
"types": ["mocha"],
"target": "es6",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
}