Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login middlend #10

Merged
merged 6 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 14 additions & 5 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const MiddleEnd = require('strange-middle-end');
const Eva = require('@eva-design/eva');
const { GestureHandlerRootView } = require('react-native-gesture-handler');
const { ApplicationProvider } = require('@ui-kitten/components');
const { default: ExpoConstants } = require('expo-constants');
const M = require('middle-end');
const Theme = require('theme');
const Routes = require('routes');
Expand All @@ -19,9 +20,7 @@ const {
OpenSans_700Bold_Italic
} = require('@expo-google-fonts/open-sans');

const middleEnd = M.create({
logErrors: process.env.NODE_ENV !== 'test'
}).initialize();
const internals = {};

const Stack = createStackNavigator();

Expand All @@ -42,8 +41,8 @@ module.exports = function App() {
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider theme={Theme}>
<ApplicationProvider {...Eva} theme={{ ...Eva.light, ...Theme }}>
<MiddleEnd.Provider middleEnd={middleEnd}>
<ReactRedux.Provider store={middleEnd.store}>
<MiddleEnd.Provider middleEnd={internals.middleEnd}>
<ReactRedux.Provider store={internals.middleEnd.store}>
<NavigationContainer>
<Stack.Navigator>
{Routes.map(({ path, component, options }) => {
Expand All @@ -67,3 +66,13 @@ module.exports = function App() {
</GestureHandlerRootView>
);
};

// e.g. 192.168.0.0:19000 (private IP on expo dev server port)
// --> 192.168.0.0:3000 (private IP on API's default port)
// hostUri is NOT set in production
internals.localUrl = () => __DEV__ && `http://${ExpoConstants.manifest?.hostUri.split(':')[0].concat(':3000')}`;

internals.middleEnd = M.create({
apiBaseUrl: ExpoConstants.manifest.extra?.apiUrl || internals.localUrl(),
logErrors: process.env.NODE_ENV !== 'test'
}).initialize();
11 changes: 11 additions & 0 deletions app.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

// https://docs.expo.io/guides/environment-variables
// https://docs.expo.io/workflow/configuration/

module.exports = ({ config }) => ({
...config,
extra: {
apiUrl: process.env.API_URL
}
});
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ module.exports = {
jest: true,
node: true
},
globals: {
__DEV__: 'readonly'
},
plugins: [
'react',
'jsx-a11y',
Expand Down
134 changes: 134 additions & 0 deletions middle-end/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const { default: Axios } = require('axios');
const AuthTokenUtils = require('../utils/auth-token');

const internals = {};

module.exports = (m, options) => {

const { apiBaseUrl } = options;

const client = internals.createClient({ baseURL: apiBaseUrl });

return {
client
};
};

internals.createClient = (options) => {

const client = Axios.create({
headers: { common: {} },
// Needed for refresh token, esp. in local development
withCredentials: true,
paramsSerializer: {
serialize: (obj) => {
// The default serializer turns array params into ?trades[]=A&trades[]=B
// which is not compatible with node's querystring parser which wants ?trades=A&trades=B.
// Luckily URLSearchParams and node are in agreement, so we can customize for it below.

if (!obj) {
return '';
}

if (typeof obj === 'string') {
return obj;
}

const params = new URLSearchParams();
const append = (key, val) => {

val = val instanceof Date ? val.toISOString() : val;

params.append(key, val);
};

Object.entries(obj).forEach(([key, value]) => {

if (Array.isArray(value)) {
value.forEach((val) => append(key, val));
}
else {
append(key, value);
}
});

return params.toString();
}
},
...options
});

client.interceptors.request.use(
async (config) => {

const accessToken = await AuthTokenUtils.get('accessToken');

if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}

return config;
}
);

client.logout = async (opts) => {

try {
await client.post('/logout', null, opts);
}
finally {
AuthTokenUtils.clear();
}
};

client.interceptors.response.use(null, async (error) => {

if (error.config?._isRetry) {
throw error;
}

const accessToken = await AuthTokenUtils.get('accessToken');
const refreshToken = await AuthTokenUtils.get('refreshToken');

// If we receive a 401, hit '/reauthorize',
// store the new token and retry the request
if (
error.config &&
error.config.reauthorize !== false &&
error.response?.status === 401 &&
accessToken &&
refreshToken
) {

error.config._isRetry = true;

try {
const { data: results } = await client.post('/reauthorize', { refreshToken }, {
reauthorize: false,
logoutOnFailure: true
});
if (results.data) {
await AuthTokenUtils.store(results.data);
}
}
catch (reauthErr) {

if (Axios.isAxiosError(reauthErr)) {
// Rethrow the original error if /reauthorize fails so that
// consumers don't have to worry about this case.
throw error;
}

// Something unexpected, non-HTTP went wrong during reauth
throw reauthErr;
}

// Retry after successful
return client.request(error.config);
}

throw error;
});

return client;
};
6 changes: 6 additions & 0 deletions middle-end/auth/action-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const MiddleEnd = require('strange-middle-end');

module.exports = MiddleEnd.createTypes('auth', {
FETCH_CURRENT_USER: MiddleEnd.type.async,
LOGIN: MiddleEnd.type.async
});
58 changes: 58 additions & 0 deletions middle-end/auth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const MiddleEnd = require('strange-middle-end');
const AuthTokenUtils = require('../../utils/auth-token');

const {
FETCH_CURRENT_USER,
LOGIN
} = require('./action-types');

const { createAction } = MiddleEnd;

module.exports = (m) => {

const getClient = () => {

const { client } = m.mods.app;
return client;
};

return {
initialize: () => m.dispatch.auth.fetchCurrentUser(),
actions: {
fetchCurrentUser: createAction(FETCH_CURRENT_USER, {
index: true,
handler: async () => {

const client = getClient();
const { data: results } = await client.get('/validate');


return results.pallie;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

}
}),
login: createAction(LOGIN, {
handler: async ({ username, password }) => {

const client = getClient();

const { data: results } = await client.post('/login', {
username,
password
}, {
reauthorize: false
});

await AuthTokenUtils.store(results);

const [err, user] = await m.dispatch.auth.fetchCurrentUser();

if (err) {
throw err;
}

return user;
}
})
}
};
};
5 changes: 5 additions & 0 deletions middle-end/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
const MiddleEnd = require('strange-middle-end');
const Redux = require('redux');
const ReduxDevtools = require('redux-devtools-extension/logOnlyInProduction');

const App = require('./app');
const Auth = require('./auth');
const Counter = require('./counter');

exports.create = (options = {}) => {

const middleEnd = MiddleEnd.create({
mods: () => ({
app: App(middleEnd, options),
auth: Auth(middleEnd, options),
counter: Counter(middleEnd, options)
}),
createStore: (reducer) => {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@
"@react-navigation/native": "^5.9.8",
"@react-navigation/stack": "^5.14.4",
"@ui-kitten/components": "^5.1.0",
"axios": "^1.1.3",
"color": "^3.1.3",
"envy": "^2.0.0",
"expo": "^45.0.0",
"expo-app-loading": "~2.0.0",
"expo-constants": "^13.2.4",
"expo-font": "~10.1.0",
"expo-secure-store": "^11.3.0",
"expo-status-bar": "~1.3.0",
"history": "^5.0.0",
"immer": "^9.0.1",
Expand Down
46 changes: 46 additions & 0 deletions utils/auth-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const SecureStore = require('expo-secure-store');

const internals = {};

exports.get = async (key) => {

const { secureStoreConf } = internals;

const token = await SecureStore.getItemAsync(key, secureStoreConf.options);

if (key === 'refreshToken') {
return JSON.parse(token);
}

return token;
};

exports.store = ({ accessToken, refreshToken } = {}) => {

const { secureStoreConf: { keys, options } } = internals;

return Promise.all([
SecureStore.setItemAsync(keys.accessToken, accessToken, options),
SecureStore.setItemAsync(keys.refreshToken, JSON.stringify(refreshToken), options)
]);
};

exports.clear = () => {

const { secureStoreConf: { keys, options } } = internals;

return Promise.all([
SecureStore.deleteItemAsync(keys.accessToken, options),
SecureStore.deleteItemAsync(keys.refreshToken, options)
]);
};

internals.secureStoreConf = {
keys: {
accessToken: 'accessToken',
refreshToken: 'refreshToken'
},
options: {
keyChainAccessible: SecureStore.WHEN_UNLOCKED
}
};