resolve merge conflicts with master

This commit is contained in:
Steve Chalco
2022-03-27 22:25:46 -07:00
89 changed files with 3748 additions and 830 deletions

2125
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Header } from './components';
import ApplicationRouter from './AppRouter';
const Application = () => {
const [options, setOptions] = useState({
value: 25,
currency: 'EUR',
countryCode: 'NL',
component: 'dropin'
});
const navigate = useNavigate();
const handleSubmit = () => {
navigate(options.component);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions(prevState => ({
...prevState,
[e.target.name]: e.target.value
}));
};
return (
<div id="app">
<Header />
<ApplicationRouter onSubmit={handleSubmit} onChange={handleChange} options={options} />
</div>
);
};
export default Application;

View File

@@ -1,14 +0,0 @@
import { Route, Routes } from 'react-router-dom';
import { PaymentsFormProps } from './types';
import { PaymentsForm, ComponentBase, CheckoutBuilder } from './components';
const ApplicationRouter = ({ options, onSubmit, onChange }: PaymentsFormProps) => {
return (
<Routes>
<Route path="/" element={<CheckoutBuilder options={options} onSubmit={onSubmit} onChange={onChange} />} />
<Route path=":component" element={<ComponentBase {...options} />} />
</Routes>
);
};
export default ApplicationRouter;

View File

@@ -0,0 +1 @@
export { userActions, configurationActions } from './reducers';

View File

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

View File

@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { ConfigurationState } from '../types';
const initialState: ConfigurationState = {
id: '',
owner: '',
name: '',
version: 0,
configuration: ''
};
export const configurationSlice = createSlice({
name: 'configuration',
initialState,
reducers: {
updateConfigurationInfo: (state, action: PayloadAction<ConfigurationState>) => {
state = action.payload;
},
clearConfigurationInfo: state => {
state = initialState;
}
}
});
export const { actions, reducer } = configurationSlice;

View File

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

View File

@@ -0,0 +1,23 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { UserState } from '../types';
const initialState: UserState = {
id: '',
username: '',
configurations: []
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
updateUserInfo: (state, action: PayloadAction<UserState>) => {
state = action.payload;
},
clearUserInfo: state => {
state = initialState;
}
}
});
export const { actions, reducer } = userSlice;

View File

@@ -0,0 +1,5 @@
import type { RootState } from '../store';
export const selectUserState = (state: RootState) => state.user;
export const selectConfigurationState = (state: RootState) => state.configuration;

View File

@@ -0,0 +1,13 @@
export type ConfigurationState = {
id: string;
owner: string;
name: string;
version: number;
configuration: string;
};
export type UserState = {
id: string;
username: string;
configurations: [ConfigurationState] | [];
};

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Header } from '.';
import ApplicationRouter from './AppRouter';
const Application = () => {
const [options, setOptions] = useState({
value: 25,
currency: 'EUR',
countryCode: 'NL',
component: 'dropin'
});
const navigate = useNavigate();
const handleSubmit = () => {
navigate(options.component);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setOptions(prevState => ({
...prevState,
[e.target.name]: e.target.value
}));
};
return (
<div id="app">
<Header />
<ApplicationRouter onSubmit={handleSubmit} onChange={handleChange} options={options} />
</div>
);
};
export default Application;

View File

@@ -0,0 +1,14 @@
import { Route, Routes } from 'react-router-dom';
import { PaymentsFormProps } from './types';
import { CheckoutBuilder,PaymentsForm, ComponentBase } from '.';
const ApplicationRouter = ({ options, onSubmit, onChange }: any) => {
return (
<Routes>
<Route path="/" element={<CheckoutBuilder options={options} onSubmit={onSubmit} onChange={onChange} />} />
<Route path=":component" element={<ComponentBase {...options} />} />
</Routes>
);
};
export default ApplicationRouter;

View File

@@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getSessions_Response } from '../../helpers/payloadSamples'; import { getSessions_Response } from '../../helpers/payloadSamples';
import { CheckoutBuilderProps } from '../../types';
import EditOptions from './EditOptions'; import EditOptions from './EditOptions';
const ApiConfig = (props: any) => { const ApiConfig = (props: any) => {

View File

@@ -9,7 +9,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { PaymentsFormProps } from '../../types';
import ApiConfig from './ApiConfig'; import ApiConfig from './ApiConfig';
import OptionalConfig from './OptionalConfig'; import OptionalConfig from './OptionalConfig';
import ProfileForm from './ProfileForm'; import ProfileForm from './ProfileForm';
@@ -20,7 +19,7 @@ const theme = createTheme();
//Create init config class //Create init config class
const CheckoutBuilder = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: PaymentsFormProps) => { const CheckoutBuilder = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: any) => {
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [configuration, setConfiguration] = useState({ const [configuration, setConfiguration] = useState({
name: '', name: '',

View File

@@ -1,8 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import EditOptions from './EditOptions';
import { CheckoutBuilderProps } from '../../types';
import { getClientConfiguration_Response } from '../../helpers/payloadSamples';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { getClientConfiguration_Response } from '../../helpers/payloadSamples';
import EditOptions from './EditOptions';
const OptionalConfig = (props: any) => { const OptionalConfig = (props: any) => {
const { configuration, setConfiguration } = props; const { configuration, setConfiguration } = props;

View File

@@ -7,7 +7,6 @@ import Select, { SelectChangeEvent } from '@mui/material/Select';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import * as React from 'react'; import * as React from 'react';
import { CheckoutBuilderProps } from '../../types';
const ProfileForm = (props: any) => { const ProfileForm = (props: any) => {
const { configuration, setConfiguration } = props; const { configuration, setConfiguration } = props;

View File

@@ -1,13 +1,15 @@
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useCheckout } from '../../hooks'; import { useCheckout } from '../../hooks';
import type { EditableCheckoutConfigFields } from '../../hooks/types';
const Component = ({ type, sessionId, sessionData }: { type: string; sessionId: string; sessionData: string }) => { const Component = ({ type, options }: { type: string; options: EditableCheckoutConfigFields }) => {
//TODO: move to own redirect handling component with useRedirect
const [redirectInfo] = useSearchParams(); const [redirectInfo] = useSearchParams();
const redirectResult = { const redirectResult = {
redirectResult: redirectInfo.get('redirectResult'), redirectResult: redirectInfo.get('redirectResult'),
redirectSessionId: redirectInfo.get('sessionId') redirectSessionId: redirectInfo.get('sessionId')
}; };
const [checkout] = useCheckout({ sessionId, sessionData, redirectResult }); const [checkout] = useCheckout(options);
if (checkout) { if (checkout) {
checkout.create(type).mount('#checkout'); checkout.create(type).mount('#checkout');

View File

@@ -1,19 +1,14 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useStartSession } from '../../hooks'; import { useInitializeCheckout } from '../../hooks';
import { FormDataProps } from '../../types'; import { InitializationRequest } from '../../hooks/types';
import Component from './Component'; import Component from './Component';
const ComponentBase = ({ value, currency, countryCode }: FormDataProps) => { const ComponentBase = (options: InitializationRequest, endpoint: string) => {
const params = useParams(); const params = useParams();
const component = params.component; const component = params.component;
const [sessionInfo] = useStartSession({ const [checkoutInfo] = useInitializeCheckout(options, component, endpoint);
value, if (checkoutInfo && component) {
currency, return <Component type={component} options={checkoutInfo} />;
countryCode,
component
});
if (sessionInfo && component) {
return <Component type={component} sessionId={sessionInfo.id} sessionData={sessionInfo.sessionData} />;
} }
return <div>Loading...</div>; return <div>Loading...</div>;
}; };

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { PaymentsFormProps } from '../../types'; import { PaymentsFormProps } from '../types';
const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: PaymentsFormProps) => { const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: any) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
onSubmit(); onSubmit();
@@ -11,9 +11,7 @@ const PaymentsForm = ({ options: { value, currency, countryCode, component }, on
onChange(e); onChange(e);
}; };
return ( return <div>PaymentForm</div>;
<div>PaymentForm</div>
);
}; };
export default PaymentsForm; export default PaymentsForm;

View File

@@ -0,0 +1,15 @@
export type FormDataProps = {
amount: {
value: number;
currency: string;
};
countryCode: string;
endpoint?: string;
component?: string;
};
export type PaymentsFormProps = {
options: FormDataProps;
onSubmit: () => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

View File

@@ -1,13 +1,12 @@
import { prepareChallengeData } from '@adyen/adyen-web/dist/types/components/ThreeDS2/components/utils'; import { FormDataProps } from '../components/types';
import { FormDataProps } from '../types';
export const compareFormData = (prev: any, next: FormDataProps) => { export const compareFormData = (prev: any, next: FormDataProps) => {
if (!prev) { if (!prev) {
return false; return false;
} }
const valueMatch = prev.value && prev.value === next.value; const valueMatch = prev.amount.value && prev.amount.value === next.amount.value;
const currencyMatch = prev.currency && prev.currency === next.currency; const currencyMatch = prev.amout.currency && prev.amount.currency === next.amount.currency;
const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode; const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode;
return countryCodeMatch && currencyMatch && valueMatch; return countryCodeMatch && currencyMatch && valueMatch;
@@ -20,3 +19,13 @@ export const compareSessionData = (prev: any, next: { sessionId: string }) => {
return prev.sessionId && prev.sessionId === next.sessionId; return prev.sessionId && prev.sessionId === next.sessionId;
}; };
export const compareCheckoutData = (prev: any, next: [any]) => {
if (!prev) {
return false;
}
const paymentMethodNames = next.map(pm => pm.name);
return Array.isArray(prev) && Array.isArray(paymentMethodNames) && prev.every((name, i) => name === paymentMethodNames[i]);
};

View File

@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
import AdyenCheckout from '@adyen/adyen-web';
import { useMemoCompare } from '../helpers/useMemoCompare';
import { compareCheckoutData } from '../../helpers';
import type { CheckoutConfig, EditableCheckoutConfigFields } from '../types';
export const useCheckout = (options: EditableCheckoutConfigFields) => {
const [checkout, setCheckout] = useState<any>(null);
// creates ref and uses data compare callback to decide if re-rendering should occur. Without this, there is an infinite loop.
const opts = useMemoCompare(options, compareCheckoutData);
useEffect(() => {
let configuration: CheckoutConfig;
if (options.session) {
configuration = {
...options,
onPaymentCompleted: (result, component) => {
console.info(result, component);
},
onError: (error, component) => {
console.error(error.name, error.message, error.stack, component);
}
};
} else {
configuration = {
...options,
onSubmit: (state, dropin) => {
console.info(state, dropin);
},
onChange: (state, dropin) => {
console.info(state, dropin);
},
onAdditionalDetails: (state, dropin) => {
console.info(state, dropin);
},
onError: error => {
console.error(error.name, error.message, error.stack);
}
};
}
const initializeCheckout: (config: object) => void = async config => {
const component = await AdyenCheckout(config);
setCheckout(component);
};
initializeCheckout(configuration);
}, [opts]);
return [checkout];
};

View File

@@ -0,0 +1,40 @@
import { useState, useEffect } from 'react';
import { useMemoCompare } from '../helpers/useMemoCompare';
import { compareFormData } from '../../helpers';
import type { InitializationRequest } from '../types';
export const useInitializeCheckout = (options: InitializationRequest, component?: string, endpoint?: string) => {
const [checkoutResponse, setCheckoutResponse] = useState<any>(null);
// creates ref and uses data compare callback to decide if re-rendering should occur. Without this, there is an infinite loop.
const opts = useMemoCompare(options, compareFormData);
useEffect(() => {
const requestOptions = {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(options)
};
const initialize: () => void = async () => {
try {
const response = await fetch(`http://localhost:8080/${endpoint}`, requestOptions);
const parsed = await response.json();
setCheckoutResponse(parsed);
} catch (err) {
if (err && typeof err === 'object') {
console.error('Error', err);
} else {
console.error('Something went wrong');
}
}
};
initialize();
}, [opts, endpoint]);
return [checkoutResponse];
};

View File

@@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
import AdyenCheckout from '@adyen/adyen-web';
import { useMemoCompare } from '../helpers/useMemoCompare';
import { compareSessionData } from '../../helpers';
import { CLIENT_KEY, ENVIRONMENT } from '../../config';
import type { EditableCheckoutConfigFields, CheckoutConfig } from '../types';
export const useRedirect = (options: EditableCheckoutConfigFields) => {
const [checkout, setCheckout] = useState<any>(null);
// creates ref and uses data compare callback to decide if re-rendering should occur. Without this, there is an infinite loop.
const opts = useMemoCompare(options, compareSessionData);
// TODO: This config will be brought in from front end. Add as argument above
useEffect(() => {
const { session: sessionInfo, redirectResult } = options;
let session;
if (redirectResult && redirectResult.redirectSessionId) {
session = { id: redirectResult.redirectSessionId };
} else if (sessionInfo) {
session = sessionInfo;
}
const configuration: CheckoutConfig = {
...options,
...session,
onPaymentCompleted: (result, component) => {
console.info(result, component);
},
onError: (error, component) => {
console.error(error.name, error.message, error.stack, component);
}
};
const initializeCheckout: (config: object) => void = async config => {
const component = await AdyenCheckout(config);
if (redirectResult && redirectResult.redirectResult && redirectResult.redirectSessionId) {
component.submitDetails({
details: { redirectResult: redirectResult.redirectResult }
});
}
setCheckout(component);
};
initializeCheckout(configuration);
}, [opts]);
return [checkout];
};

View File

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

View File

@@ -0,0 +1,43 @@
import type { PaymentAmount, PaymentMethodsResponseInterface } from '@adyen/adyen-web/dist/types/types';
export type InitializationRequest = {
merchantAccount: string;
amount: PaymentAmount;
returnUrl: string;
reference: string;
expiresAt?: Date;
countryCode?: string;
shopperLocale?: string;
shopperEmail?: string;
shopperIP?: string;
shopperReference?: string;
};
export interface EditableCheckoutConfigFields {
session?: {
id: string;
data?: string;
};
paymentMethodsResponse?: PaymentMethodsResponseInterface;
redirectResult?: {
redirectResult: string;
redirectSessionId: string;
};
environment: string;
clientKey: string;
paymentMethodsConfiguration?: object;
amount?: PaymentAmount;
showPayButton?: boolean;
}
export interface CheckoutConfig extends EditableCheckoutConfigFields {
onChange?: (state: any, element: any) => void;
onValid?: (state: any, element: any) => void;
onSubmit?: (state: any, element: any) => void;
onComplete?: (state: any, element: any) => void;
onAdditionalDetails?: (state: any, element: any) => void;
onError?: (error: any, element?: any) => void;
onPaymentCompleted?: (result: any, element: any) => void;
}
export { PaymentAmount, PaymentMethodsResponseInterface };

View File

@@ -1,72 +0,0 @@
import { useState, useEffect } from 'react';
import AdyenCheckout from '@adyen/adyen-web';
import { useMemoCompare } from './useMemoCompare';
import { compareSessionData } from '../helpers';
import { CLIENT_KEY, ENVIRONMENT } from '../config';
type SessionDataConfig = {
id: string;
sessionData?: string;
};
type CheckoutConfiguration = {
environment: string;
clientKey: string;
session: SessionDataConfig;
onPaymentCompleted: (result: any, component: any) => void;
onError: (result: any, component: any) => void;
};
export const useCheckout = (options: {
sessionId: string;
sessionData: string;
redirectResult?: {
redirectResult: string | null;
redirectSessionId: string | null;
};
}) => {
const [checkout, setCheckout] = useState<any>(null);
// creates ref and uses data compare callback to decide if re-rendering should occur. Without this, there is an infinite loop.
const opts = useMemoCompare(options, compareSessionData);
// TODO: This config will be brought in from front end. Add as argument above
useEffect(() => {
const { sessionId, sessionData, redirectResult } = options;
let session: SessionDataConfig = {
id: sessionId
};
if (redirectResult && redirectResult.redirectSessionId) {
session = { id: redirectResult.redirectSessionId };
} else {
session.sessionData = sessionData;
}
const configuration: CheckoutConfiguration = {
environment: ENVIRONMENT,
clientKey: CLIENT_KEY, // Public key used for client-side authentication: https://docs.adyen.com/development-resources/client-side-authentication
session,
onPaymentCompleted: (result, component) => {
console.info(result, component);
},
onError: (error, component) => {
console.error(error.name, error.message, error.stack, component);
}
};
const initializeCheckout: (config: object) => void = async config => {
const component = await AdyenCheckout(config);
if (redirectResult && redirectResult.redirectResult && redirectResult.redirectSessionId) {
component.submitDetails({
details: { redirectResult: redirectResult.redirectResult }
});
}
setCheckout(component);
};
initializeCheckout(configuration);
}, [opts]);
return [checkout];
};

View File

@@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import { useMemoCompare } from './useMemoCompare';
import { compareFormData } from '../helpers';
import { MERCHANT_ACCOUNT, RETURN_URL_BASE } from '../config';
type SessionConfig = {
merchantAccount: string;
amount: {
value: number;
currency: string;
};
returnUrl: string;
reference: string;
countryCode: string;
};
export const useStartSession = (options: { value: number; currency: string; countryCode: string; component: string | undefined }) => {
const [sessionInfo, setSessionInfo] = useState<any>(null);
// creates ref and uses data compare callback to decide if re-rendering should occur. Without this, there is an infinite loop.
const opts = useMemoCompare(options, compareFormData);
useEffect(() => {
const paymentData: SessionConfig = {
merchantAccount: MERCHANT_ACCOUNT,
amount: {
value: options.value * 100,
currency: options.currency
},
returnUrl: `${RETURN_URL_BASE}/${options.component}`,
reference: `${Math.floor(Math.random() * 100000000)}`,
countryCode: options.countryCode
};
const requestOptions = {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(paymentData)
};
const startSession: () => void = async () => {
try {
const response = await fetch('http://localhost:8080/startSession', requestOptions);
const parsed = await response.json();
setSessionInfo(parsed);
} catch (err) {
if (err && typeof err === 'object') {
console.error('Error', err);
} else {
console.error('Something went wrong');
}
}
};
startSession();
}, [opts]);
return [sessionInfo];
};

View File

@@ -1,13 +1,19 @@
import { render } from 'react-dom'; import { render } from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux';
import '@adyen/adyen-web/dist/adyen.css'; import '@adyen/adyen-web/dist/adyen.css';
import './index.scss'; import './index.scss';
import App from './App';
import { store } from './store';
import App from './components/App';
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
render( render(
<Provider store={store}>
<Router> <Router>
<App /> <App />
</Router>, </Router>
</Provider>,
rootElement rootElement
); );

View File

@@ -0,0 +1,13 @@
import { configureStore } from '@reduxjs/toolkit';
import { userReducer, configurationReducer } from './app';
export const store = configureStore({
reducer: {
user: userReducer,
configuration: configurationReducer
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

View File

@@ -1,37 +0,0 @@
import React from 'react';
export type FormDataProps = {
value: number;
currency: string;
countryCode: string;
component?: string;
};
export type PaymentsFormProps = {
options: FormDataProps;
onSubmit: () => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
export type CheckoutBuilderProps = {
configuration: {
name: string;
product: string;
checkout_version: string;
dropin_version: string;
optionalConfiguration: any;
apiConfiguration: any;
};
setConfiguration: any;
};
type SessionConfig = {
merchantAccount: string;
amount: {
value: number;
currency: string;
};
returnUrl: string;
reference: string;
countryCode: string;
};

View File

@@ -1,11 +0,0 @@
module.exports = {
PORT: process.env.PORT || 8080,
ADYEN_API_KEY: process.env.ADYEN_API_KEY || null,
ADYEN_BASE_URL:
process.env.ADYEN_BASE_URL || "https://checkout-test.adyen.com",
DATABASE_URL: process.env.DATABASE_URL || "mongodb://localhost/my-store",
TEST_DATABASE_URL:
process.env.TEST_DATABASE_URL || "mongodb://localhost/my-store-test",
JWT_SECRET: process.env.JWT_SECRET || "PROJECT_AW_ULTRA",
JWT_EXPIRY: process.env.JWT_EXPIRY || "1d",
};

View File

@@ -0,0 +1,7 @@
export const PORT = process.env.PORT || 8080;
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';
export const JWT_SECRET = process.env.JWT_SECRET || 'PROJECT_AW_ULTRA';
export const JWT_EXPIRY = process.env.JWT_EXPIRY || '1d';

View File

@@ -1,31 +0,0 @@
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const { DATABASE_URL } = require('./config');
const mongoOptions = {
useNewUrlParser: true,
useUnifiedTopology: true
};
const dbConnect = (url = DATABASE_URL) => {
return mongoose.connect(url, mongoOptions).catch(err => {
console.error('Mongoose failed to connect');
console.error(err);
});
};
const dbDisconnect = () => {
return mongoose.disconnect();
};
const dbGet = () => {
return mongoose;
};
module.exports = {
dbGet,
dbConnect,
dbDisconnect,
mongoOptions
};

View File

@@ -0,0 +1,23 @@
import mongoose, { ConnectOptions } from 'mongoose';
mongoose.Promise = global.Promise;
export const mongoOptions = {
useNewUrlParser: true,
useUnifiedTopology: true
};
export const dbConnect = (url: string) => {
return mongoose.connect(url, mongoOptions as ConnectOptions).catch(err => {
console.error('Mongoose failed to connect');
console.error(err);
});
};
export const dbDisconnect = () => {
return mongoose.disconnect();
};
export const dbGet = () => {
return mongoose;
};

View File

@@ -1,14 +1,15 @@
require('dotenv').config(); import path from 'path';
const path = require('path'); import express from 'express';
const express = require('express');
const mongoose = require('mongoose');
const cookieParser = require('cookie-parser');
const { dbConnect, mongoOptions } = require('./db-mongoose'); import passport from 'passport';
const { PORT, DATABASE_URL, TEST_DATABASE_URL } = require('./config'); import mongoose, { ConnectOptions } from 'mongoose';
const { authRouter, userRouter, sessionsRouter, paymentsRouter, configurationRouter } = require('./routes'); import cookieParser from 'cookie-parser';
const app = express(); import { dbConnect, mongoOptions } from './db-mongoose';
import { PORT, DATABASE_URL } from './config';
import { authRouter, userRouter, sessionsRouter, paymentsRouter, configurationRouter, localStrategy, jwtStrategy } from './routes';
export const app = express();
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
@@ -20,21 +21,24 @@ app.use(function (req, res, next) {
const root = path.join(__dirname, '../client', 'build'); const root = path.join(__dirname, '../client', 'build');
app.use(express.static(root)); app.use(express.static(root));
app.get('*', (req, res) => { app.get('/', (req, res) => {
res.sendFile('index.html', { root }); res.sendFile('index.html', { root });
}); });
passport.use(localStrategy);
passport.use(jwtStrategy);
app.use('/auth', authRouter); app.use('/auth', authRouter);
app.use('/users', userRouter); app.use('/users', userRouter);
app.use('/sessions', sessionsRouter); app.use('/sessions', sessionsRouter);
app.use('/payments', paymentsRouter); app.use('/payments', paymentsRouter);
app.use('/configurations', configurationRouter); app.use('/configurations', configurationRouter);
let server; let server: any;
const runServer = (databaseUrl = DATABASE_URL, port = PORT) => { export const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
mongoose.connect(databaseUrl, mongoOptions, err => { mongoose.connect(databaseUrl, mongoOptions as ConnectOptions, err => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@@ -51,11 +55,11 @@ const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
}); });
}; };
const closeServer = () => { export const closeServer = () => {
return mongoose.disconnect().then(() => { return mongoose.disconnect().then(() => {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
console.log('Closing server'); console.log('Closing server');
server.close(err => { return server.close((err: any) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@@ -66,8 +70,6 @@ const closeServer = () => {
}; };
if (require.main === module) { if (require.main === module) {
dbConnect(); dbConnect(DATABASE_URL);
runServer(); runServer();
} }
module.exports = { app, runServer, closeServer };

View File

@@ -1,6 +0,0 @@
const { User, Configuration } = require('./users');
module.exports = {
User,
Configuration
};

View File

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

View File

@@ -0,0 +1,19 @@
import { Document, Types } from 'mongoose';
export interface UserDocument extends Document {
_id?: Types.ObjectId;
id?: string;
username: string;
password: string;
email: string;
adyenKey?: string;
merchantAccounts?: string[];
configurations?: Types.ObjectId[];
}
export interface ConfigurationDocument extends Document {
owner: Types.ObjectId;
name: string;
version: number;
configuration: string;
}

View File

@@ -1,22 +0,0 @@
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const ConfigurationSchema = mongoose.Schema({
owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
name: { type: String, required: true },
version: { type: Number, required: true },
configuration: { type: String, required: true }
});
ConfigurationSchema.methods.apiRepr = () => ({
id: this._id,
owner: this.owner || '',
name: this.name || null,
version: this.version || [],
configuration: this.configuration
});
const Configuration = mongoose.model('Configuration', ConfigurationSchema);
module.exports = { Configuration };

View File

@@ -0,0 +1,32 @@
import { Schema, SchemaTypes, Model, model } from 'mongoose';
import { ConfigurationDocument } from '../types';
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 },
version: { type: Number, required: true },
configuration: { type: String, required: true }
});
// arrow functions not possible here, since they close over lexically enclosing context (i.e: this remains this)
ConfigurationSchema.method('apiRepr', function () {
return {
id: this._id.toString(),
owner: this.owner || '',
name: this.name || null,
version: this.version || [],
configuration: this.configuration
};
});
export const Configuration: ConfigurationModel = model<Configuration, ConfigurationModel>('Configuration', ConfigurationSchema);
export default Configuration;

View File

@@ -1,7 +0,0 @@
const { User } = require('./users');
const { Configuration } = require('./configurations');
module.exports = {
User,
Configuration
};

View File

@@ -0,0 +1,4 @@
import User from './users';
import Configuration from './configurations';
export { User, Configuration };

View File

@@ -1,41 +0,0 @@
const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
const UserSchema = mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
adyenKey: { type: String, required: false },
merchantAccounts: [{ type: String, required: false }],
configurations: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Configuration',
required: false
}
]
});
UserSchema.methods.apiRepr = () => ({
id: this._id,
username: this.username || '',
adyenKey: this.adyenKey || null,
merchantAccounts: this.merchantAccounts || [],
configurations: this.configurations || []
});
UserSchema.methods.validatePassword = password => bcrypt.compare(password, this.password);
UserSchema.statics.hashPassword = password => bcrypt.hash(password, 10);
const User = mongoose.model('User', UserSchema);
module.exports = { User };

View File

@@ -0,0 +1,61 @@
import { compare, hash } from 'bcryptjs';
import { Schema, SchemaTypes, Model, model } from 'mongoose';
import { UserDocument } from '../types';
export interface User extends UserDocument {
validatePassword(password: string): boolean;
apiRepr(): UserDocument;
}
export interface UserModel extends Model<User> {
hashPassword(password: string): string;
}
export const UserSchema: Schema = new Schema({
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: true
},
adyenKey: { type: String, required: false },
merchantAccounts: [{ type: String, required: false }],
configurations: [
{
type: SchemaTypes.ObjectId,
ref: 'Configuration',
required: false
}
]
});
// arrow functions not possible here, since they close over lexically enclosing context (i.e: this remains this)
UserSchema.method('apiRepr', function () {
return {
id: this._id.toString(),
username: this.username || '',
email: this.email || '',
adyenKey: this.adyenKey || null,
merchantAccounts: this.merchantAccounts || [],
configurations: this.configurations || []
};
});
UserSchema.method('validatePassword', function (password: string): Promise<boolean> {
return compare(password, this.password);
});
UserSchema.static('hashPassword', (password: string): Promise<string> => hash(password, 10));
export const User: UserModel = model<User, UserModel>('User', UserSchema);
export default User;

View File

@@ -4,27 +4,49 @@
"description": "Adyen Demo Back End", "description": "Adyen Demo Back End",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "ts-mocha -p ./tsconfig.json ./test/**/*.ts --exit",
"start": "node ." "start": "node ."
}, },
"author": "Mike Ossig & Hernán Chalco", "author": "Mike Ossig & Hernán Chalco",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/validator": "^13.7.1",
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"jwt-decode": "^3.1.2",
"local": "^0.3.3", "local": "^0.3.3",
"mongoose": "^6.2.4", "mongoose": "^6.2.4",
"passport": "^0.5.2", "passport": "^0.5.2",
"passport-jwt": "^4.0.0", "passport-jwt": "^4.0.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise": "^4.2.6" "request-promise": "^4.2.6",
"validator": "^13.7.0"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^2.5.1" "@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",
"mocha": "^9.2.2",
"prettier": "^2.5.1",
"ts-mocha": "^9.0.2",
"ts-node": "^10.7.0",
"typescript": "^4.6.2"
} }
} }

View File

@@ -1,7 +0,0 @@
const { router: sessionsRouter } = require('./sessions');
const { router: paymentsRouter } = require('./payments');
module.exports = {
sessionsRouter,
paymentsRouter
};

View File

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

View File

@@ -0,0 +1,71 @@
import { Request, Response, Router } from 'express';
import request from 'request-promise';
import { errorHandler } from '../helpers';
import { ADYEN_API_KEY, ADYEN_BASE_URL } from '../../config';
import type { InitializationRequest, RequestOptions } from './types';
import type { PaymentMethodsResponseInterface } from '@adyen/adyen-web/dist/types/types';
const router = Router();
router.post('/getPaymentMethods', async (req: Request, res: Response) => {
const { version, apiKey, payload }: InitializationRequest = req.body;
try {
const options: RequestOptions = {
url: `${ADYEN_BASE_URL}/${version}/paymentMethods`,
headers: {
'Content-type': 'application/json',
'x-api-key': apiKey || ADYEN_API_KEY
},
body: payload,
json: true
};
const response: PaymentMethodsResponseInterface = await request(options);
res.send(201).json(response);
} catch (err: any) {
errorHandler('/getPaymentMethods', 500, err.message, res);
}
});
router.post('/makePayment', async (req: Request, res: Response) => {
const { version, apiKey, payload } = req.body;
try {
const options: RequestOptions = {
url: `${ADYEN_BASE_URL}/${version}/payments`,
headers: {
'Content-type': 'application/json',
'x-api-key': apiKey || ADYEN_API_KEY
},
body: payload,
json: true
};
const response = await request(options);
res.send(201).json(response);
} catch (err: any) {
errorHandler('/makePayment', 500, err.message, res);
}
});
router.post('/additionalDetails', async (req: Request, res: Response) => {
const { version, apiKey, payload } = req.body;
try {
const options: RequestOptions = {
url: `${ADYEN_BASE_URL}/${version}/payments/details`,
headers: {
'Content-type': 'application/json',
'x-api-key': apiKey || ADYEN_API_KEY
},
body: payload,
json: true
};
const response = await request(options);
res.send(201).json(response);
} catch (err: any) {
errorHandler('/additionalDetails', 500, err.message, res);
}
});
export { router };

View File

@@ -1,72 +0,0 @@
const express = require("express");
const request = require("request-promise");
const mongoose = require("mongoose");
const { errorHandler } = require("../../helpers");
const { ADYEN_API_KEY, ADYEN_BASE_URL } = require("../../../config");
const router = express.Router();
mongoose.Promise = global.Promise;
router.post("/getPaymentMethods", async (req, res) => {
const { version, apiKey, payload } = req.body;
try {
const options = {
url: `${ADYEN_BASE_URL}/${version}/paymentMethods`,
headers: {
"Content-type": "application/json",
"x-api-key": apiKey || ADYEN_API_KEY,
},
body: payload,
json: true,
};
const response = await request(options);
res.send(201).json(response);
} catch (err) {
errorHandler("/getPaymentMethods", 500, err.message, res);
}
});
router.post("/makePayment", async (req, res) => {
const { version, apiKey, payload } = req.body;
try {
const options = {
url: `${ADYEN_BASE_URL}/${version}/payments`,
headers: {
"Content-type": "application/json",
"x-api-key": apiKey || ADYEN_API_KEY,
},
body: payload,
json: true,
};
const response = await request(options);
res.send(201).json(response);
} catch (err) {
errorHandler("/makePayment", 500, err.message, res);
}
});
router.post("/additionalDetails", async (req, res) => {
const { version, apiKey, payload } = req.body;
try {
const options = {
url: `${ADYEN_BASE_URL}/${version}/payments/details`,
headers: {
"Content-type": "application/json",
"x-api-key": apiKey || ADYEN_API_KEY,
},
body: payload,
json: true,
};
const response = await request(options);
res.send(201).json(response);
} catch (err) {
errorHandler("/additionalDetails", 500, err.message, res);
}
});
module.exports = { router };

View File

@@ -0,0 +1,32 @@
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, RequestOptions } from './types';
import type { CheckoutSessionSetupResponse } from '@adyen/adyen-web/dist/types/types';
const router = Router();
router.post('/sessionStart', async (req: Request, res: Response) => {
const { version, apiKey, payload }: InitializationRequest = req.body;
try {
const options: RequestOptions = {
url: `${ADYEN_BASE_URL}/${version}/sessions`,
headers: {
'Content-type': 'application/json',
'x-api-key': apiKey || ADYEN_API_KEY
},
body: payload,
json: true
};
const response: CheckoutSessionSetupResponse = await request(options);
res.send(201).json(response);
} catch (err: any) {
errorHandler('/sessionStart', 500, err.message, res);
}
});
export { router };

View File

@@ -1,32 +0,0 @@
const express = require("express");
const request = require("request-promise");
const mongoose = require("mongoose");
const { errorHandler } = require("../../helpers");
const { ADYEN_API_KEY, ADYEN_BASE_URL } = require("../../../config");
const router = express.Router();
mongoose.Promise = global.Promise;
router.post("/sessionStart", async (req, res) => {
const { version, apiKey, payload } = req.body;
try {
const options = {
url: `${ADYEN_BASE_URL}/${version}/sessions`,
headers: {
"Content-type": "application/json",
"x-api-key": apiKey || ADYEN_API_KEY,
},
body: payload,
json: true,
};
const response = await request(options);
res.send(201).json(response);
} catch (err) {
errorHandler("/sessionStart", 500, err.message, res);
}
});
module.exports = { router };

View File

@@ -0,0 +1,33 @@
import type { PaymentAmount } from '@adyen/adyen-web/dist/types/types';
export interface BaseAdyenRequest {
version: string;
apiKey?: string;
}
export interface InitializationRequest extends BaseAdyenRequest {
payload: {
merchantAccount: string;
amount: PaymentAmount;
returnUrl: string;
reference: string;
expiresAt?: Date;
countryCode?: string;
shopperLocale?: string;
shopperEmail?: string;
shopperIP?: string;
shopperReference?: string;
};
}
export interface RequestOptions {
url: string;
headers: {
'Content-type': string;
'x-api-key': string;
};
body: any;
json: boolean;
}
export type { PaymentAmount };

View File

@@ -1,33 +0,0 @@
'use strict';
const express = require('express');
const passport = require('passport');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const config = require('../../config');
const router = express.Router();
const createAuthToken = function (user) {
return jwt.sign({ user }, config.JWT_SECRET, {
subject: user.username,
expiresIn: config.JWT_EXPIRY,
algorithm: 'HS256'
});
};
const localAuth = passport.authenticate('local', { session: false });
router.use(bodyParser.json());
router.post('/login', localAuth, (req, res) => {
const authToken = createAuthToken(req.user.apiRepr());
res.json({ authToken, userId: req.user.apiRepr().id });
});
const jwtAuth = passport.authenticate('jwt', { session: false });
router.post('/refresh', jwtAuth, (req, res) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});
module.exports = { router, jwtAuth };

View File

@@ -0,0 +1,30 @@
import jwt from 'jsonwebtoken';
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): string => {
return jwt.sign({ user }, JWT_SECRET, {
subject: user.username,
expiresIn: JWT_EXPIRY,
algorithm: 'HS256'
});
};
const localAuth = passport.authenticate('local', { session: false });
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: Request, res: Response) => {
const authToken = createAuthToken(req.user);
res.json({ authToken });
});
export { router, jwtAuth };

View File

@@ -1,9 +0,0 @@
const { router: authRouter, jwtAuth } = require('./auth');
const { localStrategy, jwtStrategy } = require('./strategies');
module.exports = {
jwtAuth,
authRouter,
jwtStrategy,
localStrategy
};

View File

@@ -0,0 +1,3 @@
export { isAuthorizedForAction } from './middleware';
export { localStrategy, jwtStrategy } from './strategies';
export { router as authRouter, jwtAuth } from './auth';

View File

@@ -0,0 +1,34 @@
import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary } from 'express-serve-static-core';
import jwt_decode from 'jwt-decode';
type TokenData = {
user: {
_id: string;
username: string;
password: string;
email: string;
merchantAccounts?: [any];
configurations?: [any];
};
};
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
? next()
: res.status(401).json({
code: 401,
reason: 'Not authorized'
});
};

View File

@@ -1,49 +0,0 @@
const { Strategy: LocalStrategy } = require('passport-local');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const { User } = require('../../models');
const { JWT_SECRET } = require('../../config');
const localStrategy = new LocalStrategy((username, password, callback) => {
let user;
User.findOne({ username: username })
.then(_user => {
user = _user;
if (!user) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
return user.validatePassword(password);
})
.then(isValid => {
if (!isValid) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
return callback(null, user);
})
.catch(err => {
if (err.reason === 'LoginError') {
return callback(null, false, err);
}
return callback(err, false);
});
});
const jwtStrategy = new JwtStrategy(
{
secretOrKey: JWT_SECRET,
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
algorithms: ['HS256']
},
(payload, done) => {
done(null, payload.user);
}
);
module.exports = { localStrategy, jwtStrategy };

View File

@@ -0,0 +1,45 @@
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import { User } from '../../models';
import { JWT_SECRET } from '../../config';
export const localStrategy = new LocalStrategy(async (username, password, callback) => {
try {
const user = await User.findOne({ username: username });
if (!user) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
const isValid = await user.validatePassword(password);
if (!isValid) {
return Promise.reject({
reason: 'LoginError',
message: 'Incorrect username or password'
});
}
return callback(null, user);
} catch (err: any) {
if (err.reason === 'LoginError') {
return callback(null, false, err);
}
return callback(err, false);
}
});
export const jwtStrategy = new JwtStrategy(
{
secretOrKey: JWT_SECRET,
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
algorithms: ['HS256']
},
(payload, done) => {
done(null, payload.user);
}
);

View File

@@ -1,9 +1,7 @@
export { runUserValidation } from './users';
// TODO: Decide if we want to log to an external source. Would take up too much DB room for a free version in the meantime // TODO: Decide if we want to log to an external source. Would take up too much DB room for a free version in the meantime
const errorHandler = (endpoint, statusCode, message, res) => { export const errorHandler = (endpoint, statusCode, message, res) => {
console.error("ERROR:", endpoint, message); console.error('ERROR:', endpoint, message);
res.send(statusCode).json({ message }); res.send(statusCode).json({ message });
}; };
module.exports = {
errorHandler,
};

View File

@@ -1,5 +0,0 @@
const { runUserValidation } = require('./validations');
module.exports = {
runUserValidation
};

View File

@@ -0,0 +1 @@
export { runUserValidation } from './validations';

View File

@@ -1,4 +1,5 @@
const { User } = require('../../../models'); import isEmail from 'validator/lib/isEmail';
import { User } from '../../../models';
const checkForExistingUser = async ({ username }) => { const checkForExistingUser = async ({ username }) => {
try { try {
@@ -37,41 +38,49 @@ const checkSizedFields = reqBody => {
: checkForExistingUser(reqBody); : checkForExistingUser(reqBody);
}; };
const checkPasswordForInvalidChars = ({ password }) => { const checkEmail = reqBody => {
return isEmail(reqBody.email)
? checkSizedFields(reqBody)
: {
message: 'Invalid Email address',
location: 'reqBody.email'
};
};
const checkPasswordForInvalidChars = reqBody => {
const invalidChars = /[ ]/g; const invalidChars = /[ ]/g;
return invalidChars.test(password) return invalidChars.test(reqBody.password)
? { ? {
message: 'Password contains invalid characters', message: 'Password contains invalid characters',
location: 'Password' location: 'Password'
} }
: checkSizedFields(reqBody); : checkEmail(reqBody);
}; };
const checkUsernameForInvalidChars = ({ username }) => { const checkUsernameForInvalidChars = reqBody => {
const invalidChars = /[^A-Za-z0-9]+/g; const invalidChars = /[^A-Za-z0-9]+/g;
return invalidChars.test(username) return invalidChars.test(reqBody.username)
? { ? {
message: 'Username can only contain numbers and letters', message: 'Username can only contain numbers and letters',
location: username location: reqBody.username
} }
: checkPasswordForInvalidChars(reqBody); : checkPasswordForInvalidChars(reqBody);
}; };
const checkFieldTypes = reqBody => { const checkFieldTypes = reqBody => {
const stringFields = ['username', 'password', 'adyenKey', 'merchantAccount']; const stringFields = ['username', 'password', 'email'];
const nonStringField = stringFields.find(field => field in reqBody && typeof reqBody[field] !== 'string'); const nonStringField = stringFields.find(field => field in reqBody && typeof reqBody[field] !== 'string');
const nonStringMerchantAccount = reqBody.merchantAccount.find(field => typeof field !== 'string');
return nonStringField || nonStringMerchantAccount return nonStringField
? { ? {
message: 'Incorrect field type: expected string', message: 'Incorrect field type: expected string',
location: nonStringField || nonStringMerchantAccount location: nonStringField
} }
: checkUsernameForInvalidChars(reqBody); : checkUsernameForInvalidChars(reqBody);
}; };
const checkRequiredFields = reqBody => { const checkRequiredFields = reqBody => {
const requiredFields = ['username', 'password']; const requiredFields = ['username', 'password', 'email'];
const missingField = requiredFields.find(field => !(field in reqBody)); const missingField = requiredFields.find(field => !(field in reqBody));
return missingField return missingField
@@ -82,6 +91,4 @@ const checkRequiredFields = reqBody => {
: checkFieldTypes(reqBody); : checkFieldTypes(reqBody);
}; };
const runUserValidation = reqBody => checkRequiredFields(reqBody); export const runUserValidation = reqBody => checkRequiredFields(reqBody);
module.exports = { runUserValidation };

View File

@@ -1,11 +0,0 @@
const { authRouter } = require('./auth');
const { userRouter, configurationRouter } = require('./users');
const { sessionsRouter, paymentsRouter } = require('./adyen-endpoints');
module.exports = {
authRouter,
userRouter,
sessionsRouter,
paymentsRouter,
configurationRouter
};

View File

@@ -0,0 +1,3 @@
export { authRouter, localStrategy, jwtStrategy } from './auth';
export { userRouter, configurationRouter } from './users';
export { sessionsRouter, paymentsRouter } from './adyen-endpoints';

View File

@@ -1,60 +0,0 @@
const express = require('express');
const { jwtAuth } = require('../auth');
const { User, Configuration } = require('../../models');
const router = express.Router();
router.post('/', jwtAuth, async (req, res) => {
try {
const { owner, name, version, configuration } = req.body;
const existingUser = await User.find({ _id: owner });
if (!existingUser || !existingUser.length) {
return res.status(422).json({
code: 422,
reason: 'ValidationError',
message: 'User does not exist',
location: owner
});
}
const createdConfiguration = await Configuration.create({ owner, name, version, configuration });
return res.send(200).json({
id: createdConfiguration.id,
owner,
name,
version,
configuration
});
} catch (err) {
console.error('CONFIGURATIONS CREATION ERROR', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
router.put('/:id', jwtAuth, async (req, res) => {
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'];
updateableFields.forEach(field => {
if (field in req.body) {
toUpdate[field] = req.body[field];
}
});
const { adyenKey, merchantAccounts } = await Configuration.findOneAndUpdate({ _id: req.params.id }, { $set: toUpdate }, { new: true }).exec();
res.send(200).json({ adyenKey: adyenKey.substr(adyenKey.length - 5), merchantAccounts });
} catch (err) {
console.error('CONFIGURATIONS UPDATE ERROR', err);
res.status(500).json({ message: 'Internal server error' });
}
});
module.exports = { router };

View File

@@ -0,0 +1,108 @@
import { Router, Request, Response } from 'express';
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: Request, res: Response) => {
try {
const existingUser = await User.find({ _id: req.params.userId });
if (!existingUser || !existingUser.length) {
return res.status(422).json({
code: 422,
reason: 'Not found',
message: 'User does not exist',
location: req.params.userId
});
}
const relatedConfigurations = await Configuration.find({ owner: req.params.userId });
if (!relatedConfigurations || !relatedConfigurations.length) {
return res.status(422).json({
code: 422,
reason: 'Not found',
message: 'No related configurations',
location: req.params.userId
});
}
res.status(201).json(relatedConfigurations.map(config => config.apiRepr()));
} 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: Request, res: Response) => {
try {
const existingConfiguration = await Configuration.find({ _id: req.params.id });
if (!existingConfiguration || !existingConfiguration.length) {
return res.status(404).json({
code: 404,
reason: 'Not found',
message: 'Configuration by this ID does not exist',
location: req.body.id
});
}
res.status(201).json(existingConfiguration[0].apiRepr());
} catch (err) {
console.error('ERROR GETTING CONFIGURATION', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
router.post('/:userId', jwtAuth, isAuthorizedForAction, async (req: Request, res: Response) => {
try {
const { owner, name, version, configuration } = req.body;
const existingUser = await User.find({ _id: owner });
if (!existingUser || !existingUser.length) {
return res.status(422).json({
code: 422,
reason: 'ValidationError',
message: 'User does not exist',
location: owner
});
}
const createdConfiguration = await Configuration.create({ owner, name, version, configuration });
res.status(200).json(createdConfiguration.apiRepr());
} catch (err) {
console.error('CONFIGURATIONS CREATION ERROR', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
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: ConfigToUpdate = {};
const updateableFields = ['name' as const, 'version' as const, 'configuration' as const];
updateableFields.forEach(field => {
if (field in req.body) {
toUpdate[field] = req.body[field];
}
});
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' });
}
});
export { router };

View File

@@ -1,7 +0,0 @@
const { router: userRouter } = require('./users');
const { router: configurationRouter } = require('./configurations');
module.exports = {
userRouter,
configurationRouter
};

View File

@@ -0,0 +1,2 @@
export { router as userRouter } from './users';
export { router as configurationRouter } from './configurations';

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,88 +0,0 @@
const express = require('express');
const { jwtAuth } = require('../auth');
const { runValidation } = require('../helpers/users');
const router = express.Router();
const { User } = require('../../models');
router.get('/:id', jwtAuth, async (req, res) => {
try {
const existingUser = await User.find({ _id: req.body.id });
if (!existingUser || !existingUser.length) {
return res.status(404).json({
code: 404,
reason: 'Not found',
message: 'User by this ID does not exist',
location: req.body.id
});
}
const { username, adyenKey, merchantAccounts, configurations } = user.apiRepr();
res.status(201).send({
username,
adyenKey: adyenKey.substr(adyenKey.length - 5),
merchantAccounts,
configurations
});
} catch (err) {
console.error('ERROR GETTING USER', err);
}
});
router.post('/', async (req, res) => {
try {
const invalidEntry = await runValidation(req.body);
if (invalidEntry) {
return res.status(422).json({
code: 422,
reason: 'ValidationError',
message: invalidEntry.message,
location: invalidEntry.location
});
}
const { username, password } = req.body;
const hashedPassword = await User.hashPassword(password);
const createdUser = await User.create({ username, password: hashedPassword });
const { id, adyenKey, merchantAccounts, configurations } = createdUser.apiRepr();
return res.send(200).json({
id,
username,
adyenKey: adyenKey.substr(adyenKey.length - 5),
merchantAccounts,
configurations
});
} catch (err) {
console.error('USER CREATION ERROR', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
router.put('/:id', jwtAuth, async (req, res) => {
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 = ['adyenKey', 'merchantAccounts'];
updateableFields.forEach(field => {
if (field in req.body) {
toUpdate[field] = req.body[field];
}
});
const { adyenKey, merchantAccounts } = await User.findOneAndUpdate({ _id: req.params.id }, { $set: toUpdate }, { new: true }).exec();
res.send(200).json({ adyenKey: adyenKey.substr(adyenKey.length - 5), merchantAccounts });
} catch (err) {
console.error('USER UPDATE ERROR', err);
res.status(500).json({ message: 'Internal server error' });
}
});
module.exports = { router };

View File

@@ -0,0 +1,96 @@
import { Router } from 'express';
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 {
const invalidEntry = await runUserValidation(req.body);
if (invalidEntry) {
return res.status(422).json({
code: 422,
reason: 'ValidationError',
message: invalidEntry.message,
location: invalidEntry.location
});
}
const { username, password, email } = req.body;
const hashedPassword = await User.hashPassword(password);
const createdUser = await User.create({ username, password: hashedPassword, email });
const { id } = createdUser.apiRepr();
return res.status(200).json({
id,
username,
email
});
} catch (err) {
console.error('USER CREATION ERROR', err);
res.status(500).json({ code: 500, message: 'Internal server error' });
}
});
router.get('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
try {
const existingUser = await User.find({ _id: req.params.userId });
if (!existingUser || !existingUser.length) {
return res.status(404).json({
code: 404,
reason: 'Not found',
message: 'User by this ID does not exist',
location: req.params.userId
});
}
const { id, username, email, adyenKey, merchantAccounts, configurations } = existingUser[0].apiRepr();
res.status(201).send({
id,
username,
email,
adyenKey: adyenKey && adyenKey.length ? adyenKey.substr(adyenKey.length - 5) : '',
merchantAccounts,
configurations
});
} catch (err) {
console.error('ERROR GETTING USER', err);
}
});
router.put('/:userId', jwtAuth, isAuthorizedForAction, async (req, res) => {
if (!(req.params.userId === req.body.id)) {
const message = `Request patch id (${req.params.userId} and request body id (${req.body.id}) must match)`;
console.error(message);
res.status(400).json({ message: message });
}
try {
const toUpdate: UserToUpdate = {};
const updateableFields = ['adyenKey' as const, 'merchantAccounts' as const, 'configurations' as const];
updateableFields.forEach(field => {
if (field in req.body) {
toUpdate[field] = req.body[field];
}
});
const foundUser = await User.findOneAndUpdate({ _id: req.body.id }, { $set: toUpdate }, { new: true }).exec();
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' });
}
});
export { router };

View File

@@ -0,0 +1,40 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import { app } from '../../index';
import { userTestData } from '../structures';
chai.use(chaiHttp);
const { testUserData, testConfigData } = userTestData;
export const createMockUser = (): any => {
return chai
.request(app)
.post('/users')
.send(testUserData)
.then(res => res)
.catch(err => console.log(err));
};
export const logUserIn = (): any => {
return chai
.request(app)
.post('/auth/login')
.send({ username: testUserData.username, password: testUserData.password })
.then(res => res.body.authToken)
.catch(err => console.log(err));
};
export const createMockConfigurations = async (): Promise<any> => {
const user = await createMockUser();
const authToken = await logUserIn();
const userId = user.body.id;
testConfigData.owner = userId;
return chai
.request(app)
.post(`/configurations/${userId}`)
.set('Authorization', `Bearer ${authToken}`)
.send(testConfigData)
.then(res => ({ mockConfig: res, authToken, userId }))
.catch(err => console.log(err));
};

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

@@ -0,0 +1,73 @@
import chai from 'chai';
import mongoose from 'mongoose';
import chaiHttp from 'chai-http';
import { userTestData } from '../../structures';
import { TEST_DATABASE_URL } from '../../../config';
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
.dropDatabase()
.then(result => resolve(result))
.catch(err => reject(err));
});
};
describe('Configurations API', () => {
const configFields = ['id', 'owner', 'name', 'version', 'configuration'];
before(() => {
return runServer(TEST_DATABASE_URL);
});
afterEach(() => {
return tearDownDb();
});
after(() => {
return closeServer();
});
it('Should create a configuration on POST', async () => {
const { mockConfig } = await createMockConfigurations();
const hasKeys = configFields.reduce((acc, x) => acc && mockConfig.body.hasOwnProperty(x));
assert.equal(mockConfig.status, 200, 'failed status check');
assert.isTrue(hasKeys, 'failed key compare');
});
it('Should reject request for config with wrong auth token', async () => {
const { mockConfig, userId } = await createMockConfigurations();
let agent = chai.request.agent(app);
return agent
.get(`/configurations/${userId}/${mockConfig.body.id}`)
.set('Authorization', `Bearer ${userTestData.wrongAuthToken}`)
.then(res => {
assert.equal(res.status, 401, 'failed status check');
return res;
});
});
it('Should send back a configuration by id', async () => {
const { mockConfig, authToken, userId } = await createMockConfigurations();
let agent = chai.request.agent(app);
return agent
.get(`/configurations/${userId}/${mockConfig.body.id}`)
.set('Authorization', `Bearer ${authToken}`)
.then(res => {
const hasKeys = configFields.reduce((acc, x) => acc && res.body.hasOwnProperty(x));
assert.equal(res.status, 201, 'failed status check');
assert.equal(typeof res.body, 'object', 'failed res body');
assert.equal(res.body.id, mockConfig.body.id, 'failed id check');
assert.isTrue(hasKeys, 'failed key compare');
return res;
});
});
});

View File

@@ -0,0 +1,88 @@
import chai from 'chai';
import mongoose from 'mongoose';
import chaiHttp from 'chai-http';
import { userTestData } from '../../structures';
import { TEST_DATABASE_URL } from '../../../config';
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
.dropDatabase()
.then(result => resolve(result))
.catch(err => reject(err));
});
};
describe('Users API', () => {
const userFields = ['id', 'username', 'email'];
before(() => {
return runServer(TEST_DATABASE_URL);
});
afterEach(() => {
return tearDownDb();
});
after(() => {
return closeServer();
});
it('Should create a user on POST', async () => {
const mockUser = await createMockUser();
const hasKeys = userFields.reduce((acc, x) => acc && mockUser.body.hasOwnProperty(x));
assert.equal(mockUser.status, 200, 'failed status check');
assert.isTrue(hasKeys, 'failed key compare');
});
it('Should send back a user by id', async () => {
const mockUser = await createMockUser();
const authToken = await logUserIn();
let agent = chai.request.agent(app);
return agent
.get(`/users/${mockUser.body.id}`)
.set('Authorization', `Bearer ${authToken}`)
.then(res => {
const hasKeys = [...userFields, 'adyenKey', 'merchantAccounts', 'configurations'].reduce((acc, x) => acc && res.body.hasOwnProperty(x));
assert.equal(res.status, 201, 'failed status check');
assert.equal(typeof res.body, 'object', 'failed body type compare');
assert.equal(res.body.id, mockUser.body.id, 'failed id compare');
assert.isTrue(hasKeys, 'failed key compare');
return res;
});
});
it('Should update users on PUT', async () => {
const mockUser = await createMockUser();
const authToken = await logUserIn();
const mockPayload = {
id: mockUser.body.id,
adyenKey: userTestData.wrongAuthToken,
merchantAccounts: ['TestMerchant1', 'TestMerchant2']
};
let agent = chai.request.agent(app);
return agent
.put(`/users/${mockUser.body.id}`)
.set('Authorization', `Bearer ${authToken}`)
.send(mockPayload)
.then(res => {
const hasValues = mockPayload.merchantAccounts.reduce((acc, x) => acc && res.body.merchantAccounts.includes(x));
assert.equal(res.status, 200, 'failed status check');
assert.equal(typeof res.body, 'object', 'failed body type compare');
assert.equal(res.body.id, mockPayload.id, 'failed id compare');
assert.equal(res.body.adyenKey, mockPayload.adyenKey.substring(mockPayload.adyenKey.length - 5), 'failed adyenKey compare');
assert.equal(res.body.merchantAccounts.length, 2, 'failed length compare');
assert.isTrue(hasValues, 'failed value compare');
return res;
});
});
});

View File

@@ -0,0 +1,5 @@
import sessionsResponse from './sessionsResponse.json';
import paymentsResponse from './paymentsResponse.json';
import paymentMethodsResponse from './paymentMethodsResponse.json';
export { sessionsResponse, paymentsResponse, paymentMethodsResponse };

View File

@@ -0,0 +1,40 @@
{
"paymentMethods": [
{
"brands": ["visa", "mc", "discover", "cup", "maestro", "diners", "jcb"],
"name": "Credit Card",
"type": "scheme"
},
{
"configuration": {
"intent": "capture"
},
"name": "PayPal",
"type": "paypal"
},
{
"name": "AliPay",
"type": "alipay"
},
{
"name": "UnionPay",
"type": "unionpay"
},
{
"name": "ACH Direct Debit",
"type": "ach"
},
{
"name": "WeChat Pay",
"type": "wechatpayMiniProgram"
},
{
"name": "WeChat Pay",
"type": "wechatpayQR"
},
{
"name": "WeChat Pay",
"type": "wechatpayWeb"
}
]
}

View File

@@ -0,0 +1,36 @@
{
"additionalData": {
"avsResult": "5 No AVS data provided",
"eci": "07",
"threeDSVersion": "1.0.2",
"acquirerAccountCode": "TestPmmAcquirerAccountMarketPlace",
"xid": "N/A",
"cavvAlgorithm": "N/A",
"cardBin": "411111",
"threeDAuthenticated": "false",
"paymentMethodVariant": "visa",
"merchantReference": "Your order number",
"cardIssuingCountry": "NL",
"liabilityShift": "false",
"authCode": "077666",
"cardHolderName": "John Smith",
"threeDOffered": "true",
"cardIssuingBank": "ADYEN TEST BANK",
"threeDOfferedResponse": "N",
"authorisationMid": "900",
"issuerCountry": "NL",
"cvcResult": "1 Matches",
"cavv": "N/A",
"threeDAuthenticatedResponse": "N/A",
"threeds2.cardEnrolled": "false",
"paymentMethod": "visa",
"cardPaymentMethod": "visa"
},
"pspReference": "S99S35XPMSGLNK82",
"resultCode": "Authorised",
"amount": {
"currency": "USD",
"value": 1000
},
"merchantReference": "Your order number"
}

View File

@@ -0,0 +1,13 @@
{
"amount": {
"currency": "USD",
"value": 1000
},
"countryCode": "NL",
"expiresAt": "2022-03-16T16:59:13+01:00",
"id": "CS586835B3512556CE",
"merchantAccount": "MikeOssig",
"reference": "YOUR_PAYMENT_REFERENCE",
"returnUrl": "https://your-company.com/checkout?shopperOrder=12xy..",
"sessionData": "Ab02b4c0!BQABAgBPT6JboEAomd9Nz1SP/Vh3pcbBlAVOGBeV6IIEzyiESJvGheLpJI6iqOvNJx/NoLv5kaGiRk2e2AKcgbf0y71N4lIHJ0LuCgyXRSo2GQ9ioAymsq0IcgD7rwltoTrK0A7hH10STuz1VSLvPGgN0g+pu1rWXvBhAFf5c13nnBhd7DPfEPkOGRbUePrY9XqNK1zdOu5uZsY3RLrusRNLXn7Vt4X8ffKUT8eguLt0atIkRvWiEZtCTzU7xQhIiCltM1N9Fp8uwKrPeurObn6M/PH+3rGs0y4QWcvYyiXVULasO7aLl5mxBWF0O5Rl3r+YomFF8rvWYuaGGJFJvUrl2+Mx5xAbfQi+2uBWgAihAbb9H4CY7HhhWpf/mL0aEnBBvyYS8UC65C0K0b4T0KWRMsaS64DtTPtltzNdVszrUbEGNeNuXduJ2DlTJNNy0KBARe7+WRzS2gTDwAmN4VoES5n73ZgjnHsaOw1AZIIqRIVKpEzoJfYdhkOadKUppDsaUEP4ItNU7vqBFAzg46PdDQHrDFxQaaU89tyYZR0le3n6FbdBJOugwE3DBbUhz6wvk66lnm31rKKuyo79NoBE/WdyiKjPMvB2saf+aCR/ZSksUoHXba8vAugIi1/nXyMjo7uqGnnZbttO3b+Ydfc6xQuaH3GIJygxnO4+cIUD+vMQuxAUiV9H4C18OTRt20uy9TD7AEp7ImtleSI6IkFGMEFBQTEwM0NBNTM3RUFFRDg3QzI0REQ1MzkwOUI4MEE3OEE5MjNFMzgyM0Q2OERBQ0M5NEI5RkY4MzA1REMifSpMSO+9zCitGJwddVlL6Qt8Gmrbgu3fmUy8ZphL3Cm7LqxJA+oxQky3QwhCqwJtONcUnqMf72Nkw8AYDfaXVt/gfnBNvz0VX6jmtoSn7Xso1YkdOGIxt7hUQxwoLLCpuoZaaUQtzJiWh9js11lvGR6D2cFXFs+Hm1JPNpgxTZm/REzrN43l+BbreJB9egobCPPFHtc7tZAjyp0rzR7Whf7922MHY3n+2xFn0aRZMnuWJsOwQkT3NGCB43272FF5OyoyuYIHRcmvJQ+GxQ6Uaq555bj702rH8/L4+XZdADC4UR1vD8ZFHY4FhTSq/NBGWWeabxdHD0gxQA/SQXP89AaOzjyvR5YYoRyrxwxA3uB/qPyLndaViJ4ismUHlwSbSPCC0gG1hHZGxAErqCTltcVA8PUNrU9BPqIGzQ=="
}

View File

@@ -0,0 +1,2 @@
export * as adyenTestData from './adyen-responses';
export * as userTestData from './users';

View File

@@ -0,0 +1,7 @@
import testUserData from './testUser.json';
import testConfigData from './testConfigs.json';
const wrongAuthToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiNjIzMzc5YjljZjQyNjVjMDNmYjc4ZmI0IiwidXNlcm5hbWUiOiJ1c2VybmFtZSIsImVtYWlsIjoidGVzdEB0ZXN0LmNvbSIsImFkeWVuS2V5IjpudWxsLCJtZXJjaGFudEFjY291bnRzIjpbXSwiY29uZmlndXJhdGlvbnMiOltdfSwiaWF0IjoxNjQ3NTQwNjY1LCJleHAiOjE2NDc2MjcwNjUsInN1YiI6InVzZXJuYW1lIn0.QCj6r6-nM9s8unajhUTc3He7Ga_tSxs_rW_FwxVqsNo';
export { testUserData, testConfigData, wrongAuthToken };

View File

@@ -0,0 +1,7 @@
{
"id": "62323c39295cd4f531dc3346",
"owner": "62323c39295cd4f531dc3341",
"name": "testConfig-1",
"version": 1,
"configuration": "{\"testField\": \"active\"}"
}

View File

@@ -0,0 +1,5 @@
{
"username": "username",
"password": "password",
"email": "test@test.com"
}

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"types": ["mocha"],
"target": "es6",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}