mirror of
https://github.com/jlengrand/adyen-web-demo.git
synced 2026-03-09 23:51:23 +00:00
resolve merge conflicts with master
This commit is contained in:
2125
package-lock.json
generated
2125
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
1
packages/client/src/app/actions.ts
Normal file
1
packages/client/src/app/actions.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { userActions, configurationActions } from './reducers';
|
||||
5
packages/client/src/app/index.ts
Normal file
5
packages/client/src/app/index.ts
Normal 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';
|
||||
26
packages/client/src/app/reducers/configuration.ts
Normal file
26
packages/client/src/app/reducers/configuration.ts
Normal 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;
|
||||
2
packages/client/src/app/reducers/index.ts
Normal file
2
packages/client/src/app/reducers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { reducer as userReducer, actions as userActions } from './user';
|
||||
export { reducer as configurationReducer, actions as configurationActions } from './configuration';
|
||||
23
packages/client/src/app/reducers/user.ts
Normal file
23
packages/client/src/app/reducers/user.ts
Normal 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;
|
||||
5
packages/client/src/app/selectors.ts
Normal file
5
packages/client/src/app/selectors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export const selectUserState = (state: RootState) => state.user;
|
||||
|
||||
export const selectConfigurationState = (state: RootState) => state.configuration;
|
||||
13
packages/client/src/app/types.ts
Normal file
13
packages/client/src/app/types.ts
Normal 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] | [];
|
||||
};
|
||||
34
packages/client/src/components/App.tsx
Normal file
34
packages/client/src/components/App.tsx
Normal 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;
|
||||
14
packages/client/src/components/AppRouter.tsx
Normal file
14
packages/client/src/components/AppRouter.tsx
Normal 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;
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getSessions_Response } from '../../helpers/payloadSamples';
|
||||
import { CheckoutBuilderProps } from '../../types';
|
||||
import EditOptions from './EditOptions';
|
||||
|
||||
const ApiConfig = (props: any) => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { PaymentsFormProps } from '../../types';
|
||||
import ApiConfig from './ApiConfig';
|
||||
import OptionalConfig from './OptionalConfig';
|
||||
import ProfileForm from './ProfileForm';
|
||||
@@ -20,7 +19,7 @@ const theme = createTheme();
|
||||
//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 [configuration, setConfiguration] = useState({
|
||||
name: '',
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 { getClientConfiguration_Response } from '../../helpers/payloadSamples';
|
||||
import EditOptions from './EditOptions';
|
||||
|
||||
const OptionalConfig = (props: any) => {
|
||||
const { configuration, setConfiguration } = props;
|
||||
|
||||
@@ -7,7 +7,6 @@ import Select, { SelectChangeEvent } from '@mui/material/Select';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import * as React from 'react';
|
||||
import { CheckoutBuilderProps } from '../../types';
|
||||
|
||||
const ProfileForm = (props: any) => {
|
||||
const { configuration, setConfiguration } = props;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useCheckout } from '../../hooks';
|
||||
import type { EditableCheckoutConfigFields } from '../../hooks/types';
|
||||
|
||||
const Component = ({ type, sessionId, sessionData }: { type: string; sessionId: string; sessionData: string }) => {
|
||||
const [redirectInfo] = useSearchParams();
|
||||
const redirectResult = {
|
||||
redirectResult: redirectInfo.get('redirectResult'),
|
||||
redirectSessionId: redirectInfo.get('sessionId')
|
||||
};
|
||||
const [checkout] = useCheckout({ sessionId, sessionData, redirectResult });
|
||||
const Component = ({ type, options }: { type: string; options: EditableCheckoutConfigFields }) => {
|
||||
//TODO: move to own redirect handling component with useRedirect
|
||||
const [redirectInfo] = useSearchParams();
|
||||
const redirectResult = {
|
||||
redirectResult: redirectInfo.get('redirectResult'),
|
||||
redirectSessionId: redirectInfo.get('sessionId')
|
||||
};
|
||||
const [checkout] = useCheckout(options);
|
||||
|
||||
if (checkout) {
|
||||
checkout.create(type).mount('#checkout');
|
||||
}
|
||||
return <div id="checkout"></div>;
|
||||
if (checkout) {
|
||||
checkout.create(type).mount('#checkout');
|
||||
}
|
||||
return <div id="checkout"></div>;
|
||||
};
|
||||
|
||||
export default Component;
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useStartSession } from '../../hooks';
|
||||
import { FormDataProps } from '../../types';
|
||||
import { useInitializeCheckout } from '../../hooks';
|
||||
import { InitializationRequest } from '../../hooks/types';
|
||||
import Component from './Component';
|
||||
|
||||
const ComponentBase = ({ value, currency, countryCode }: FormDataProps) => {
|
||||
const params = useParams();
|
||||
const component = params.component;
|
||||
const [sessionInfo] = useStartSession({
|
||||
value,
|
||||
currency,
|
||||
countryCode,
|
||||
component
|
||||
});
|
||||
if (sessionInfo && component) {
|
||||
return <Component type={component} sessionId={sessionInfo.id} sessionData={sessionInfo.sessionData} />;
|
||||
}
|
||||
return <div>Loading...</div>;
|
||||
const ComponentBase = (options: InitializationRequest, endpoint: string) => {
|
||||
const params = useParams();
|
||||
const component = params.component;
|
||||
const [checkoutInfo] = useInitializeCheckout(options, component, endpoint);
|
||||
if (checkoutInfo && component) {
|
||||
return <Component type={component} options={checkoutInfo} />;
|
||||
}
|
||||
return <div>Loading...</div>;
|
||||
};
|
||||
|
||||
export default ComponentBase;
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import React from 'react';
|
||||
import { PaymentsFormProps } from '../../types';
|
||||
import { PaymentsFormProps } from '../types';
|
||||
|
||||
const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: PaymentsFormProps) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
};
|
||||
const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: any) => {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e);
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>PaymentForm</div>
|
||||
);
|
||||
return <div>PaymentForm</div>;
|
||||
};
|
||||
|
||||
export default PaymentsForm;
|
||||
|
||||
15
packages/client/src/components/types.ts
Normal file
15
packages/client/src/components/types.ts
Normal 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;
|
||||
};
|
||||
@@ -1,22 +1,31 @@
|
||||
import { prepareChallengeData } from '@adyen/adyen-web/dist/types/components/ThreeDS2/components/utils';
|
||||
import { FormDataProps } from '../types';
|
||||
import { FormDataProps } from '../components/types';
|
||||
|
||||
export const compareFormData = (prev: any, next: FormDataProps) => {
|
||||
if (!prev) {
|
||||
return false;
|
||||
}
|
||||
if (!prev) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const valueMatch = prev.value && prev.value === next.value;
|
||||
const currencyMatch = prev.currency && prev.currency === next.currency;
|
||||
const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode;
|
||||
const valueMatch = prev.amount.value && prev.amount.value === next.amount.value;
|
||||
const currencyMatch = prev.amout.currency && prev.amount.currency === next.amount.currency;
|
||||
const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode;
|
||||
|
||||
return countryCodeMatch && currencyMatch && valueMatch;
|
||||
return countryCodeMatch && currencyMatch && valueMatch;
|
||||
};
|
||||
|
||||
export const compareSessionData = (prev: any, next: { sessionId: string }) => {
|
||||
if (!prev) {
|
||||
return false;
|
||||
}
|
||||
if (!prev) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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]);
|
||||
};
|
||||
|
||||
52
packages/client/src/hooks/checkout/useCheckout.ts
Normal file
52
packages/client/src/hooks/checkout/useCheckout.ts
Normal 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];
|
||||
};
|
||||
40
packages/client/src/hooks/checkout/useInitializeCheckout.ts
Normal file
40
packages/client/src/hooks/checkout/useInitializeCheckout.ts
Normal 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];
|
||||
};
|
||||
52
packages/client/src/hooks/checkout/useRedirect.ts
Normal file
52
packages/client/src/hooks/checkout/useRedirect.ts
Normal 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];
|
||||
};
|
||||
@@ -1,4 +1,10 @@
|
||||
import { useCheckout } from './useCheckout';
|
||||
import { useStartSession } from './useStartSession';
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
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;
|
||||
|
||||
43
packages/client/src/hooks/types.ts
Normal file
43
packages/client/src/hooks/types.ts
Normal 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 };
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import { render } from 'react-dom';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import '@adyen/adyen-web/dist/adyen.css';
|
||||
import './index.scss';
|
||||
import App from './App';
|
||||
|
||||
import { store } from './store';
|
||||
import App from './components/App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>,
|
||||
rootElement
|
||||
<App />
|
||||
</Router>
|
||||
</Provider>,
|
||||
rootElement
|
||||
);
|
||||
|
||||
13
packages/client/src/store.ts
Normal file
13
packages/client/src/store.ts
Normal 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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
7
packages/server/config.ts
Normal file
7
packages/server/config.ts
Normal 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';
|
||||
@@ -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
|
||||
};
|
||||
23
packages/server/db-mongoose.ts
Normal file
23
packages/server/db-mongoose.ts
Normal 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;
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
require('dotenv').config();
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const mongoose = require('mongoose');
|
||||
const cookieParser = require('cookie-parser');
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
|
||||
const { dbConnect, mongoOptions } = require('./db-mongoose');
|
||||
const { PORT, DATABASE_URL, TEST_DATABASE_URL } = require('./config');
|
||||
const { authRouter, userRouter, sessionsRouter, paymentsRouter, configurationRouter } = require('./routes');
|
||||
import passport from 'passport';
|
||||
import mongoose, { ConnectOptions } from 'mongoose';
|
||||
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(cookieParser());
|
||||
|
||||
@@ -20,21 +21,24 @@ app.use(function (req, res, next) {
|
||||
|
||||
const root = path.join(__dirname, '../client', 'build');
|
||||
app.use(express.static(root));
|
||||
app.get('*', (req, res) => {
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile('index.html', { root });
|
||||
});
|
||||
|
||||
passport.use(localStrategy);
|
||||
passport.use(jwtStrategy);
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
app.use('/users', userRouter);
|
||||
app.use('/sessions', sessionsRouter);
|
||||
app.use('/payments', paymentsRouter);
|
||||
app.use('/configurations', configurationRouter);
|
||||
|
||||
let server;
|
||||
let server: any;
|
||||
|
||||
const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
mongoose.connect(databaseUrl, mongoOptions, err => {
|
||||
export const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
mongoose.connect(databaseUrl, mongoOptions as ConnectOptions, err => {
|
||||
if (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 new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log('Closing server');
|
||||
server.close(err => {
|
||||
return server.close((err: any) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
@@ -66,8 +70,6 @@ const closeServer = () => {
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
dbConnect();
|
||||
dbConnect(DATABASE_URL);
|
||||
runServer();
|
||||
}
|
||||
|
||||
module.exports = { app, runServer, closeServer };
|
||||
@@ -1,6 +0,0 @@
|
||||
const { User, Configuration } = require('./users');
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Configuration
|
||||
};
|
||||
2
packages/server/models/index.ts
Normal file
2
packages/server/models/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { User, Configuration } from './users';
|
||||
export type { UserDocument, ConfigurationDocument } from './types';
|
||||
19
packages/server/models/types.ts
Normal file
19
packages/server/models/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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 };
|
||||
32
packages/server/models/users/configurations.ts
Normal file
32
packages/server/models/users/configurations.ts
Normal 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;
|
||||
@@ -1,7 +0,0 @@
|
||||
const { User } = require('./users');
|
||||
const { Configuration } = require('./configurations');
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
Configuration
|
||||
};
|
||||
4
packages/server/models/users/index.ts
Normal file
4
packages/server/models/users/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import User from './users';
|
||||
import Configuration from './configurations';
|
||||
|
||||
export { User, Configuration };
|
||||
@@ -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 };
|
||||
61
packages/server/models/users/users.ts
Normal file
61
packages/server/models/users/users.ts
Normal 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;
|
||||
@@ -4,27 +4,49 @@
|
||||
"description": "Adyen Demo Back End",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "ts-mocha -p ./tsconfig.json ./test/**/*.ts --exit",
|
||||
"start": "node ."
|
||||
},
|
||||
"author": "Mike Ossig & Hernán Chalco",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.7.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"local": "^0.3.3",
|
||||
"mongoose": "^6.2.4",
|
||||
"passport": "^0.5.2",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6"
|
||||
"request-promise": "^4.2.6",
|
||||
"validator": "^13.7.0"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
const { router: sessionsRouter } = require('./sessions');
|
||||
const { router: paymentsRouter } = require('./payments');
|
||||
|
||||
module.exports = {
|
||||
sessionsRouter,
|
||||
paymentsRouter
|
||||
};
|
||||
4
packages/server/routes/adyen-endpoints/index.ts
Normal file
4
packages/server/routes/adyen-endpoints/index.ts
Normal 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';
|
||||
71
packages/server/routes/adyen-endpoints/payments.ts
Normal file
71
packages/server/routes/adyen-endpoints/payments.ts
Normal 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 };
|
||||
@@ -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 };
|
||||
32
packages/server/routes/adyen-endpoints/sessions.ts
Normal file
32
packages/server/routes/adyen-endpoints/sessions.ts
Normal 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 };
|
||||
@@ -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 };
|
||||
33
packages/server/routes/adyen-endpoints/types.ts
Normal file
33
packages/server/routes/adyen-endpoints/types.ts
Normal 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 };
|
||||
@@ -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 };
|
||||
30
packages/server/routes/auth/auth.ts
Normal file
30
packages/server/routes/auth/auth.ts
Normal 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 };
|
||||
@@ -1,9 +0,0 @@
|
||||
const { router: authRouter, jwtAuth } = require('./auth');
|
||||
const { localStrategy, jwtStrategy } = require('./strategies');
|
||||
|
||||
module.exports = {
|
||||
jwtAuth,
|
||||
authRouter,
|
||||
jwtStrategy,
|
||||
localStrategy
|
||||
};
|
||||
3
packages/server/routes/auth/index.ts
Normal file
3
packages/server/routes/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { isAuthorizedForAction } from './middleware';
|
||||
export { localStrategy, jwtStrategy } from './strategies';
|
||||
export { router as authRouter, jwtAuth } from './auth';
|
||||
34
packages/server/routes/auth/middleware.ts
Normal file
34
packages/server/routes/auth/middleware.ts
Normal 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'
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
45
packages/server/routes/auth/strategies.ts
Normal file
45
packages/server/routes/auth/strategies.ts
Normal 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);
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
const errorHandler = (endpoint, statusCode, message, res) => {
|
||||
console.error("ERROR:", endpoint, message);
|
||||
export const errorHandler = (endpoint, statusCode, message, res) => {
|
||||
console.error('ERROR:', endpoint, message);
|
||||
res.send(statusCode).json({ message });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
errorHandler,
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
const { runUserValidation } = require('./validations');
|
||||
|
||||
module.exports = {
|
||||
runUserValidation
|
||||
};
|
||||
1
packages/server/routes/helpers/users/index.ts
Normal file
1
packages/server/routes/helpers/users/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { runUserValidation } from './validations';
|
||||
@@ -1,4 +1,5 @@
|
||||
const { User } = require('../../../models');
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { User } from '../../../models';
|
||||
|
||||
const checkForExistingUser = async ({ username }) => {
|
||||
try {
|
||||
@@ -37,41 +38,49 @@ const checkSizedFields = 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;
|
||||
return invalidChars.test(password)
|
||||
return invalidChars.test(reqBody.password)
|
||||
? {
|
||||
message: 'Password contains invalid characters',
|
||||
location: 'Password'
|
||||
}
|
||||
: checkSizedFields(reqBody);
|
||||
: checkEmail(reqBody);
|
||||
};
|
||||
|
||||
const checkUsernameForInvalidChars = ({ username }) => {
|
||||
const checkUsernameForInvalidChars = reqBody => {
|
||||
const invalidChars = /[^A-Za-z0-9]+/g;
|
||||
return invalidChars.test(username)
|
||||
return invalidChars.test(reqBody.username)
|
||||
? {
|
||||
message: 'Username can only contain numbers and letters',
|
||||
location: username
|
||||
location: reqBody.username
|
||||
}
|
||||
: checkPasswordForInvalidChars(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 nonStringMerchantAccount = reqBody.merchantAccount.find(field => typeof field !== 'string');
|
||||
|
||||
return nonStringField || nonStringMerchantAccount
|
||||
return nonStringField
|
||||
? {
|
||||
message: 'Incorrect field type: expected string',
|
||||
location: nonStringField || nonStringMerchantAccount
|
||||
location: nonStringField
|
||||
}
|
||||
: checkUsernameForInvalidChars(reqBody);
|
||||
};
|
||||
|
||||
const checkRequiredFields = reqBody => {
|
||||
const requiredFields = ['username', 'password'];
|
||||
const requiredFields = ['username', 'password', 'email'];
|
||||
const missingField = requiredFields.find(field => !(field in reqBody));
|
||||
|
||||
return missingField
|
||||
@@ -82,6 +91,4 @@ const checkRequiredFields = reqBody => {
|
||||
: checkFieldTypes(reqBody);
|
||||
};
|
||||
|
||||
const runUserValidation = reqBody => checkRequiredFields(reqBody);
|
||||
|
||||
module.exports = { runUserValidation };
|
||||
export const runUserValidation = reqBody => checkRequiredFields(reqBody);
|
||||
@@ -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
|
||||
};
|
||||
3
packages/server/routes/index.ts
Normal file
3
packages/server/routes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { authRouter, localStrategy, jwtStrategy } from './auth';
|
||||
export { userRouter, configurationRouter } from './users';
|
||||
export { sessionsRouter, paymentsRouter } from './adyen-endpoints';
|
||||
@@ -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 };
|
||||
108
packages/server/routes/users/configurations.ts
Normal file
108
packages/server/routes/users/configurations.ts
Normal 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 };
|
||||
@@ -1,7 +0,0 @@
|
||||
const { router: userRouter } = require('./users');
|
||||
const { router: configurationRouter } = require('./configurations');
|
||||
|
||||
module.exports = {
|
||||
userRouter,
|
||||
configurationRouter
|
||||
};
|
||||
2
packages/server/routes/users/index.ts
Normal file
2
packages/server/routes/users/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { router as userRouter } from './users';
|
||||
export { router as configurationRouter } from './configurations';
|
||||
11
packages/server/routes/users/types.ts
Normal file
11
packages/server/routes/users/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type ConfigToUpdate = {
|
||||
name?: string;
|
||||
version?: number;
|
||||
configuration?: string;
|
||||
};
|
||||
|
||||
export type UserToUpdate = {
|
||||
adyenKey?: string;
|
||||
merchantAccounts?: string[];
|
||||
configurations?: string[];
|
||||
};
|
||||
@@ -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 };
|
||||
96
packages/server/routes/users/users.ts
Normal file
96
packages/server/routes/users/users.ts
Normal 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 };
|
||||
40
packages/server/test/helpers/helpers.ts
Normal file
40
packages/server/test/helpers/helpers.ts
Normal 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));
|
||||
};
|
||||
1
packages/server/test/helpers/index.ts
Normal file
1
packages/server/test/helpers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as userHelpers from './helpers';
|
||||
197
packages/server/test/routes/auth/auth.test.ts
Normal file
197
packages/server/test/routes/auth/auth.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
73
packages/server/test/routes/users/configurations.test.ts
Normal file
73
packages/server/test/routes/users/configurations.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
88
packages/server/test/routes/users/users.test.ts
Normal file
88
packages/server/test/routes/users/users.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
5
packages/server/test/structures/adyen-responses/index.ts
Normal file
5
packages/server/test/structures/adyen-responses/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import sessionsResponse from './sessionsResponse.json';
|
||||
import paymentsResponse from './paymentsResponse.json';
|
||||
import paymentMethodsResponse from './paymentMethodsResponse.json';
|
||||
|
||||
export { sessionsResponse, paymentsResponse, paymentMethodsResponse };
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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=="
|
||||
}
|
||||
2
packages/server/test/structures/index.ts
Normal file
2
packages/server/test/structures/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * as adyenTestData from './adyen-responses';
|
||||
export * as userTestData from './users';
|
||||
7
packages/server/test/structures/users/index.ts
Normal file
7
packages/server/test/structures/users/index.ts
Normal 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 };
|
||||
7
packages/server/test/structures/users/testConfigs.json
Normal file
7
packages/server/test/structures/users/testConfigs.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "62323c39295cd4f531dc3346",
|
||||
"owner": "62323c39295cd4f531dc3341",
|
||||
"name": "testConfig-1",
|
||||
"version": 1,
|
||||
"configuration": "{\"testField\": \"active\"}"
|
||||
}
|
||||
5
packages/server/test/structures/users/testUser.json
Normal file
5
packages/server/test/structures/users/testUser.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"email": "test@test.com"
|
||||
}
|
||||
11
packages/server/tsconfig.json
Normal file
11
packages/server/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"types": ["mocha"],
|
||||
"target": "es6",
|
||||
"rootDir": "./",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user