mirror of
https://github.com/jlengrand/adyen-web-demo.git
synced 2026-03-10 08:01:24 +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 * as React from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getSessions_Response } from '../../helpers/payloadSamples';
|
import { getSessions_Response } from '../../helpers/payloadSamples';
|
||||||
import { CheckoutBuilderProps } from '../../types';
|
|
||||||
import EditOptions from './EditOptions';
|
import EditOptions from './EditOptions';
|
||||||
|
|
||||||
const ApiConfig = (props: any) => {
|
const ApiConfig = (props: any) => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { createTheme, ThemeProvider } from '@mui/material/styles';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PaymentsFormProps } from '../../types';
|
|
||||||
import ApiConfig from './ApiConfig';
|
import ApiConfig from './ApiConfig';
|
||||||
import OptionalConfig from './OptionalConfig';
|
import OptionalConfig from './OptionalConfig';
|
||||||
import ProfileForm from './ProfileForm';
|
import ProfileForm from './ProfileForm';
|
||||||
@@ -20,7 +19,7 @@ const theme = createTheme();
|
|||||||
//Create init config class
|
//Create init config class
|
||||||
|
|
||||||
|
|
||||||
const CheckoutBuilder = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: PaymentsFormProps) => {
|
const CheckoutBuilder = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: any) => {
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [configuration, setConfiguration] = useState({
|
const [configuration, setConfiguration] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import EditOptions from './EditOptions';
|
|
||||||
import { CheckoutBuilderProps } from '../../types';
|
|
||||||
import { getClientConfiguration_Response } from '../../helpers/payloadSamples';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { getClientConfiguration_Response } from '../../helpers/payloadSamples';
|
||||||
|
import EditOptions from './EditOptions';
|
||||||
|
|
||||||
const OptionalConfig = (props: any) => {
|
const OptionalConfig = (props: any) => {
|
||||||
const { configuration, setConfiguration } = props;
|
const { configuration, setConfiguration } = props;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import Select, { SelectChangeEvent } from '@mui/material/Select';
|
|||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CheckoutBuilderProps } from '../../types';
|
|
||||||
|
|
||||||
const ProfileForm = (props: any) => {
|
const ProfileForm = (props: any) => {
|
||||||
const { configuration, setConfiguration } = props;
|
const { configuration, setConfiguration } = props;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useCheckout } from '../../hooks';
|
import { useCheckout } from '../../hooks';
|
||||||
|
import type { EditableCheckoutConfigFields } from '../../hooks/types';
|
||||||
|
|
||||||
const Component = ({ type, sessionId, sessionData }: { type: string; sessionId: string; sessionData: string }) => {
|
const Component = ({ type, options }: { type: string; options: EditableCheckoutConfigFields }) => {
|
||||||
|
//TODO: move to own redirect handling component with useRedirect
|
||||||
const [redirectInfo] = useSearchParams();
|
const [redirectInfo] = useSearchParams();
|
||||||
const redirectResult = {
|
const redirectResult = {
|
||||||
redirectResult: redirectInfo.get('redirectResult'),
|
redirectResult: redirectInfo.get('redirectResult'),
|
||||||
redirectSessionId: redirectInfo.get('sessionId')
|
redirectSessionId: redirectInfo.get('sessionId')
|
||||||
};
|
};
|
||||||
const [checkout] = useCheckout({ sessionId, sessionData, redirectResult });
|
const [checkout] = useCheckout(options);
|
||||||
|
|
||||||
if (checkout) {
|
if (checkout) {
|
||||||
checkout.create(type).mount('#checkout');
|
checkout.create(type).mount('#checkout');
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useStartSession } from '../../hooks';
|
import { useInitializeCheckout } from '../../hooks';
|
||||||
import { FormDataProps } from '../../types';
|
import { InitializationRequest } from '../../hooks/types';
|
||||||
import Component from './Component';
|
import Component from './Component';
|
||||||
|
|
||||||
const ComponentBase = ({ value, currency, countryCode }: FormDataProps) => {
|
const ComponentBase = (options: InitializationRequest, endpoint: string) => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const component = params.component;
|
const component = params.component;
|
||||||
const [sessionInfo] = useStartSession({
|
const [checkoutInfo] = useInitializeCheckout(options, component, endpoint);
|
||||||
value,
|
if (checkoutInfo && component) {
|
||||||
currency,
|
return <Component type={component} options={checkoutInfo} />;
|
||||||
countryCode,
|
|
||||||
component
|
|
||||||
});
|
|
||||||
if (sessionInfo && component) {
|
|
||||||
return <Component type={component} sessionId={sessionInfo.id} sessionData={sessionInfo.sessionData} />;
|
|
||||||
}
|
}
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PaymentsFormProps } from '../../types';
|
import { PaymentsFormProps } from '../types';
|
||||||
|
|
||||||
const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: PaymentsFormProps) => {
|
const PaymentsForm = ({ options: { value, currency, countryCode, component }, onSubmit, onChange }: any) => {
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
@@ -11,9 +11,7 @@ const PaymentsForm = ({ options: { value, currency, countryCode, component }, on
|
|||||||
onChange(e);
|
onChange(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <div>PaymentForm</div>;
|
||||||
<div>PaymentForm</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaymentsForm;
|
export default PaymentsForm;
|
||||||
|
|||||||
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,13 +1,12 @@
|
|||||||
import { prepareChallengeData } from '@adyen/adyen-web/dist/types/components/ThreeDS2/components/utils';
|
import { FormDataProps } from '../components/types';
|
||||||
import { FormDataProps } from '../types';
|
|
||||||
|
|
||||||
export const compareFormData = (prev: any, next: FormDataProps) => {
|
export const compareFormData = (prev: any, next: FormDataProps) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueMatch = prev.value && prev.value === next.value;
|
const valueMatch = prev.amount.value && prev.amount.value === next.amount.value;
|
||||||
const currencyMatch = prev.currency && prev.currency === next.currency;
|
const currencyMatch = prev.amout.currency && prev.amount.currency === next.amount.currency;
|
||||||
const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode;
|
const countryCodeMatch = prev.countryCode && prev.countryCode === next.countryCode;
|
||||||
|
|
||||||
return countryCodeMatch && currencyMatch && valueMatch;
|
return countryCodeMatch && currencyMatch && valueMatch;
|
||||||
@@ -20,3 +19,13 @@ export const compareSessionData = (prev: any, next: { sessionId: string }) => {
|
|||||||
|
|
||||||
return prev.sessionId && prev.sessionId === next.sessionId;
|
return prev.sessionId && prev.sessionId === next.sessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const compareCheckoutData = (prev: any, next: [any]) => {
|
||||||
|
if (!prev) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethodNames = next.map(pm => pm.name);
|
||||||
|
|
||||||
|
return Array.isArray(prev) && Array.isArray(paymentMethodNames) && prev.every((name, i) => name === paymentMethodNames[i]);
|
||||||
|
};
|
||||||
|
|||||||
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 { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||||
import { useStartSession } from './useStartSession';
|
import type { RootState, AppDispatch } from '../store';
|
||||||
|
|
||||||
export { useCheckout, useStartSession };
|
export type { InitializationRequest, EditableCheckoutConfigFields, CheckoutConfig, PaymentAmount, PaymentMethodsResponseInterface } from './types';
|
||||||
|
|
||||||
|
export { useCheckout } from './checkout/useCheckout';
|
||||||
|
export { useInitializeCheckout } from './checkout/useInitializeCheckout';
|
||||||
|
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||||
|
|||||||
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 { render } from 'react-dom';
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import '@adyen/adyen-web/dist/adyen.css';
|
import '@adyen/adyen-web/dist/adyen.css';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
import App from './App';
|
|
||||||
|
import { store } from './store';
|
||||||
|
import App from './components/App';
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
render(
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<App />
|
<App />
|
||||||
</Router>,
|
</Router>
|
||||||
|
</Provider>,
|
||||||
rootElement
|
rootElement
|
||||||
);
|
);
|
||||||
|
|||||||
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();
|
import path from 'path';
|
||||||
const path = require('path');
|
import express from 'express';
|
||||||
const express = require('express');
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
const cookieParser = require('cookie-parser');
|
|
||||||
|
|
||||||
const { dbConnect, mongoOptions } = require('./db-mongoose');
|
import passport from 'passport';
|
||||||
const { PORT, DATABASE_URL, TEST_DATABASE_URL } = require('./config');
|
import mongoose, { ConnectOptions } from 'mongoose';
|
||||||
const { authRouter, userRouter, sessionsRouter, paymentsRouter, configurationRouter } = require('./routes');
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
const app = express();
|
import { dbConnect, mongoOptions } from './db-mongoose';
|
||||||
|
import { PORT, DATABASE_URL } from './config';
|
||||||
|
import { authRouter, userRouter, sessionsRouter, paymentsRouter, configurationRouter, localStrategy, jwtStrategy } from './routes';
|
||||||
|
|
||||||
|
export const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
@@ -20,21 +21,24 @@ app.use(function (req, res, next) {
|
|||||||
|
|
||||||
const root = path.join(__dirname, '../client', 'build');
|
const root = path.join(__dirname, '../client', 'build');
|
||||||
app.use(express.static(root));
|
app.use(express.static(root));
|
||||||
app.get('*', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile('index.html', { root });
|
res.sendFile('index.html', { root });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
passport.use(localStrategy);
|
||||||
|
passport.use(jwtStrategy);
|
||||||
|
|
||||||
app.use('/auth', authRouter);
|
app.use('/auth', authRouter);
|
||||||
app.use('/users', userRouter);
|
app.use('/users', userRouter);
|
||||||
app.use('/sessions', sessionsRouter);
|
app.use('/sessions', sessionsRouter);
|
||||||
app.use('/payments', paymentsRouter);
|
app.use('/payments', paymentsRouter);
|
||||||
app.use('/configurations', configurationRouter);
|
app.use('/configurations', configurationRouter);
|
||||||
|
|
||||||
let server;
|
let server: any;
|
||||||
|
|
||||||
const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
|
export const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
mongoose.connect(databaseUrl, mongoOptions, err => {
|
mongoose.connect(databaseUrl, mongoOptions as ConnectOptions, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
@@ -51,11 +55,11 @@ const runServer = (databaseUrl = DATABASE_URL, port = PORT) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeServer = () => {
|
export const closeServer = () => {
|
||||||
return mongoose.disconnect().then(() => {
|
return mongoose.disconnect().then(() => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
console.log('Closing server');
|
console.log('Closing server');
|
||||||
server.close(err => {
|
return server.close((err: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
@@ -66,8 +70,6 @@ const closeServer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
dbConnect();
|
dbConnect(DATABASE_URL);
|
||||||
runServer();
|
runServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { app, runServer, closeServer };
|
|
||||||
@@ -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",
|
"description": "Adyen Demo Back End",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "ts-mocha -p ./tsconfig.json ./test/**/*.ts --exit",
|
||||||
"start": "node ."
|
"start": "node ."
|
||||||
},
|
},
|
||||||
"author": "Mike Ossig & Hernán Chalco",
|
"author": "Mike Ossig & Hernán Chalco",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/validator": "^13.7.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"local": "^0.3.3",
|
"local": "^0.3.3",
|
||||||
"mongoose": "^6.2.4",
|
"mongoose": "^6.2.4",
|
||||||
"passport": "^0.5.2",
|
"passport": "^0.5.2",
|
||||||
"passport-jwt": "^4.0.0",
|
"passport-jwt": "^4.0.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise": "^4.2.6"
|
"request-promise": "^4.2.6",
|
||||||
|
"validator": "^13.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.5.1"
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/chai": "^4.3.0",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
|
"@types/expect": "^24.3.0",
|
||||||
|
"@types/express": "^4.17.13",
|
||||||
|
"@types/express-serve-static-core": "^4.17.28",
|
||||||
|
"@types/jsonwebtoken": "^8.5.8",
|
||||||
|
"@types/mocha": "^9.1.0",
|
||||||
|
"@types/mongoose": "^5.11.97",
|
||||||
|
"@types/passport": "^1.0.7",
|
||||||
|
"@types/passport-jwt": "^3.0.6",
|
||||||
|
"@types/passport-local": "^1.0.34",
|
||||||
|
"@types/request-promise": "^4.1.48",
|
||||||
|
"chai": "^4.3.6",
|
||||||
|
"chai-http": "^4.3.0",
|
||||||
|
"mocha": "^9.2.2",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"ts-mocha": "^9.0.2",
|
||||||
|
"ts-node": "^10.7.0",
|
||||||
|
"typescript": "^4.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// TODO: Decide if we want to log to an external source. Would take up too much DB room for a free version in the meantime
|
||||||
const errorHandler = (endpoint, statusCode, message, res) => {
|
export const errorHandler = (endpoint, statusCode, message, res) => {
|
||||||
console.error("ERROR:", endpoint, message);
|
console.error('ERROR:', endpoint, message);
|
||||||
res.send(statusCode).json({ message });
|
res.send(statusCode).json({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
errorHandler,
|
|
||||||
};
|
|
||||||
@@ -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 }) => {
|
const checkForExistingUser = async ({ username }) => {
|
||||||
try {
|
try {
|
||||||
@@ -37,41 +38,49 @@ const checkSizedFields = reqBody => {
|
|||||||
: checkForExistingUser(reqBody);
|
: checkForExistingUser(reqBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkPasswordForInvalidChars = ({ password }) => {
|
const checkEmail = reqBody => {
|
||||||
|
return isEmail(reqBody.email)
|
||||||
|
? checkSizedFields(reqBody)
|
||||||
|
: {
|
||||||
|
message: 'Invalid Email address',
|
||||||
|
location: 'reqBody.email'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPasswordForInvalidChars = reqBody => {
|
||||||
const invalidChars = /[ ]/g;
|
const invalidChars = /[ ]/g;
|
||||||
return invalidChars.test(password)
|
return invalidChars.test(reqBody.password)
|
||||||
? {
|
? {
|
||||||
message: 'Password contains invalid characters',
|
message: 'Password contains invalid characters',
|
||||||
location: 'Password'
|
location: 'Password'
|
||||||
}
|
}
|
||||||
: checkSizedFields(reqBody);
|
: checkEmail(reqBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkUsernameForInvalidChars = ({ username }) => {
|
const checkUsernameForInvalidChars = reqBody => {
|
||||||
const invalidChars = /[^A-Za-z0-9]+/g;
|
const invalidChars = /[^A-Za-z0-9]+/g;
|
||||||
return invalidChars.test(username)
|
return invalidChars.test(reqBody.username)
|
||||||
? {
|
? {
|
||||||
message: 'Username can only contain numbers and letters',
|
message: 'Username can only contain numbers and letters',
|
||||||
location: username
|
location: reqBody.username
|
||||||
}
|
}
|
||||||
: checkPasswordForInvalidChars(reqBody);
|
: checkPasswordForInvalidChars(reqBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkFieldTypes = reqBody => {
|
const checkFieldTypes = reqBody => {
|
||||||
const stringFields = ['username', 'password', 'adyenKey', 'merchantAccount'];
|
const stringFields = ['username', 'password', 'email'];
|
||||||
const nonStringField = stringFields.find(field => field in reqBody && typeof reqBody[field] !== 'string');
|
const nonStringField = stringFields.find(field => field in reqBody && typeof reqBody[field] !== 'string');
|
||||||
const nonStringMerchantAccount = reqBody.merchantAccount.find(field => typeof field !== 'string');
|
|
||||||
|
|
||||||
return nonStringField || nonStringMerchantAccount
|
return nonStringField
|
||||||
? {
|
? {
|
||||||
message: 'Incorrect field type: expected string',
|
message: 'Incorrect field type: expected string',
|
||||||
location: nonStringField || nonStringMerchantAccount
|
location: nonStringField
|
||||||
}
|
}
|
||||||
: checkUsernameForInvalidChars(reqBody);
|
: checkUsernameForInvalidChars(reqBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkRequiredFields = reqBody => {
|
const checkRequiredFields = reqBody => {
|
||||||
const requiredFields = ['username', 'password'];
|
const requiredFields = ['username', 'password', 'email'];
|
||||||
const missingField = requiredFields.find(field => !(field in reqBody));
|
const missingField = requiredFields.find(field => !(field in reqBody));
|
||||||
|
|
||||||
return missingField
|
return missingField
|
||||||
@@ -82,6 +91,4 @@ const checkRequiredFields = reqBody => {
|
|||||||
: checkFieldTypes(reqBody);
|
: checkFieldTypes(reqBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
const runUserValidation = reqBody => checkRequiredFields(reqBody);
|
export const runUserValidation = reqBody => checkRequiredFields(reqBody);
|
||||||
|
|
||||||
module.exports = { runUserValidation };
|
|
||||||
@@ -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