Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Merge pull request #83 from shocknet/auth-preface
Browse files Browse the repository at this point in the history
Auth preface
  • Loading branch information
Daniel Lugo authored Apr 1, 2021
2 parents 9d3bb88 + f9d77d6 commit b5ecefa
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 20 deletions.
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
"SHOCKWALLET",
"Unmount",
"autocorrect",
"healthz",
"immer",
"luxon",
"persistor",
"uuidv",
"qrcode",
"reduxjs",
"serv",
"shockping",
"uuidv",
"videojs",
"youtube"
]
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
},
"devDependencies": {
"@types/classnames": "^2.2.11",
"@types/luxon": "^1.26.2",
"@types/react-router-dom": "5.x.x",
"@types/socket.io-client": "^1.4.35",
"eslint-config-prettier": "^8.1.0",
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions src/actions/NodeActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const unlockWallet = ({ alias, password }) => async dispatch => {
alias: data.user.alias,
authToken: data.authorization,
publicKey: data.user.publicKey,
// @ts-expect-error
authTokenExpirationDate: decodedToken.exp
}
});
Expand All @@ -188,6 +189,7 @@ export const createAlias = ({ alias, password }) => async dispatch => {
alias: data.user.alias,
authToken: data.authorization,
publicKey: data.user.publicKey,
// @ts-expect-error
authTokenExpirationDate: decodedToken.exp
}
});
Expand Down Expand Up @@ -220,6 +222,7 @@ export const createWallet = ({ alias, password }) => async dispatch => {
alias: data.user.alias,
authToken: data.authorization,
publicKey: data.user.publicKey,
// @ts-expect-error
authTokenExpirationDate: decodedToken.exp
}
});
Expand Down
28 changes: 14 additions & 14 deletions src/pages/Feed/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import React, {
useMemo,
useState
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import * as Common from "shock-common";

import { processDisplayName } from "../../utils/String";
import { attachMedia } from "../../utils/Torrents";
Expand All @@ -16,7 +17,6 @@ import BottomBar from "../../common/BottomBar";
import UserIcon from "./components/UserIcon";
import SendTipModal from "./components/SendTipModal";
import Loader from "../../common/Loader";
import ShockAvatar from "../../common/ShockAvatar";

import { subscribeFollows } from "../../actions/FeedActions";

Expand All @@ -28,9 +28,9 @@ const SharedPost = React.lazy(() => import("../../common/Post/SharedPost"));

const FeedPage = () => {
const dispatch = useDispatch();
const follows = useSelector(({ feed }) => feed.follows);
const posts = useSelector(({ feed }) => feed.posts);
const userProfiles = useSelector(({ userProfiles }) => userProfiles);
const follows = Store.useSelector(({ feed }) => feed.follows);
const posts = Store.useSelector(({ feed }) => feed.posts);
const userProfiles = Store.useSelector(({ userProfiles }) => userProfiles);
const [tipModalData, setTipModalOpen] = useState(null);
const [unlockModalData, setUnlockModalOpen] = useState(null);
const { avatar } = Store.useSelector(Store.selectSelfUser);
Expand Down Expand Up @@ -77,14 +77,8 @@ const FeedPage = () => {
}, [dispatch]);

useEffect(() => {
const subscription = startFollowsSubscription();

return async () => {
const resolvedSubscription = await subscription;
resolvedSubscription.off("*");
resolvedSubscription.close();
};
}, [dispatch]);
startFollowsSubscription();
}, [dispatch, startFollowsSubscription]);

useLayoutEffect(() => {
attachMedia(
Expand All @@ -106,11 +100,15 @@ const FeedPage = () => {
<div className="following-bar-list">
{follows?.map(follow => {
const publicKey = follow.user;
const profile = userProfiles[publicKey] ?? {};
const profile =
userProfiles[publicKey] ?? Common.createEmptyUser(publicKey);
return (
<UserIcon
username={processDisplayName(publicKey, profile.displayName)}
avatar={`data:image/png;base64,${profile.avatar}`}
addButton={undefined}
large={undefined}
main={undefined}
/>
);
})}
Expand Down Expand Up @@ -162,6 +160,8 @@ const FeedPage = () => {
openUnlockModal={toggleUnlockModal}
// TODO: User online status handling
isOnlineNode
tipCounter={undefined}
tipValue={undefined}
/>
</Suspense>
);
Expand Down
50 changes: 50 additions & 0 deletions src/reducers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Reducers

## Guidelines

- Define an state interface, named according to the reducer, e.g. `interface NodeState { foo: string }`.
- Declare an initial state, of the state type, e.g: `const INITIAL_STATE: NodeState = { foo: 'baz' }`.
- Import `AnyAction` from `redux`, this will be the type for the `action` the reducer accepts.
- Either import the actions enum from an action creators file, e.g. `import { NODE_ACTIONS } from '../actions/NodeActions'` or use a single action creator's `.match()` method if it was created via `createAction` from `@reduxjs/toolkit`:

```ts
import { AnyAction } from "redux";
import { NODE_ACTIONS, setFoo } from "../actions/NodeActions";
// ...
const node = (state: NodeState, action: ShockAction) => {
try {
if (action.type === NODE_ACTIONS.setFoo) {
// foo will NOT be typed as string
const { foo } = action.payload;
return { ...state, foo };
}
if (setFoo.match(action)) {
// foo will be typed as string
const { foo } = action.payload;
return { ...state, foo };
}
} catch (e) {
logger.error(`Error inside Node reducer:`);
logger.error(e);
}
};
```

- Prefer `immer` over homegrown immutable data, especially for complex updates, the resulting code will be cleaner/shorter/less buggier. Small example:

```ts
const node = produce((draft: NodeState, action: ShockAction) => {
try {
if (action.type === "setFoo") {
draft.foo = action.data.foo;
}
} catch (e) {
logger.error(`Error inside Node reducer:`);
logger.error(e);
}
}, INITIAL_STATE);
```

- Wrap every reducer's body in a try-catch. Inside the catch block, if the reducer can reasonably handle the error, then it should handle it, otherwise, return the current state, always log the error and pre-pend a `logger.error('Error inside X reducer:')` before logging the actual error.

- Only one default export per file, the reducer, if something else "needs" to be exported, then it doesn't belong in a reducer file. E.g. typings should go into `shock-common`.
30 changes: 26 additions & 4 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createStore, applyMiddleware, compose } from "redux";
import { createStore, applyMiddleware, compose, AnyAction } from "redux";
import thunk from "redux-thunk";
import rootReducer, { State } from "../reducers";
import { useSelector as origUseSelector } from "react-redux";
Expand All @@ -7,6 +7,9 @@ import storage from "redux-persist/lib/storage"; // defaults to localStorage for
import Migrations from "./Migrations";
import createMigrate from "redux-persist/es/createMigrate";
import autoMergeLevel2 from "redux-persist/es/stateReconciler/autoMergeLevel2";
import createSagaMiddleware from "redux-saga";

import rootSaga, { _setStore as setSagaStore } from "./sagas";

const persistConfig = {
key: "root",
Expand All @@ -19,18 +22,37 @@ const persistConfig = {
})
};

const persistedReducer = persistReducer(persistConfig, rootReducer);
const persistedReducer = persistReducer<State, AnyAction>(
persistConfig,
rootReducer
);

const initializeStore = () => {
const sagaMiddleware = createSagaMiddleware();
const appliedMiddleware = applyMiddleware(thunk, sagaMiddleware);
// @ts-expect-error
const store = window.__REDUX_DEVTOOLS_EXTENSION__
? createStore(
persistedReducer,
// @ts-expect-error
compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__())
compose(appliedMiddleware, window.__REDUX_DEVTOOLS_EXTENSION__())
)
: createStore(persistedReducer, applyMiddleware(thunk));
: createStore(persistedReducer, appliedMiddleware);
let persistor = persistStore(store);
setSagaStore(store);
sagaMiddleware.run(rootSaga);
// In the future if polls (which cause ticks in the store) are moved to sagas
// they will be dependant on the ping socket, we need a keep alive tick for
// when the are no actions being dispatched making the store tick and
// therefore the ping saga realizing the socket died, if it did so. Ideally,
// the ping/socket subscription should emit a timeout event of such but I'd
// rather do that when I learn Event Channels and implement the ping socket
// using that.
setInterval(() => {
store.dispatch({
type: "shock::keepAlive"
});
}, 20000);
return { store, persistor };
};

Expand Down
33 changes: 33 additions & 0 deletions src/store/sagas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Sagas

Sagas are preferred over thunks because:

- No overloading of `dispatch()`.
- More expressive power.
- Called on every store tick so they can react to state changes.

## Guidelines

- Sagas can import from services, actions and selectors but not from reducers.
- Only one default export per file, a saga named according to the file name and scope of the code, e.g. `nodeSaga` in `node.ts`, if something else "needs" to be exported, then it doesn't belong in a saga file. E.g. in utils. That saga can then run other sagas as needed. Naming helps error traces.
- Use `getStore()` from `./_store` and not from elsewhere.
- In the future we should switch to channel events to avoid the `getStore()` conundrum.
- Ping saga handles online state.
- Build and connect sockets/polls when `Selectors.isReady(yield select())` returns true, disconnect them and tear them down when false, this simplifies token/user handling.
- Call `_setStore()` on store creation but before running the root saga.
- Wrap every single saga's body in a try-catch. Inside the catch block, if the saga can reasonably handle the error, then it should handle it, always log the error and pre-pend a `logger.error('Error inside [sagaName]* ()')` before logging the actual error. E.g.:

```ts
function* handlePing() {
try {
// ...
} catch (e) {
logger.error("Error inside handlePing* ()");
logger.error(e);
}
}
```

- Do not do conditional dispatch based on state inside the saga, let the reducer handle the action as it seems fit, reducers are pure and are more easily testable, only check for conditions inside of those.
- Use `Common.YieldReturn<T>` for type safety.
- There's one exception to all of these rules and that is the `_store` file.
13 changes: 13 additions & 0 deletions src/store/sagas/_store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Store, AnyAction } from "redux";

import { State } from "../../reducers";

type ShockStore = Store<State, AnyAction>;

let currStore = {} as ShockStore;

export const _setStore = (store: ShockStore) => {
currStore = store;
};

export const getStore = () => currStore;
13 changes: 13 additions & 0 deletions src/store/sagas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { all } from "redux-saga/effects";

import node from "./node";
import ping from "./ping";

import { _setStore } from "./_store";

function* rootSaga() {
yield all([node, ping]);
}

export { _setStore };
export default rootSaga;
31 changes: 31 additions & 0 deletions src/store/sagas/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { all, takeEvery } from "redux-saga/effects";

import * as Utils from "../../utils";

// eslint-disable-next-line require-yield
function* handleTokenInvalidation() {
try {
// Utils.navigate("/");
} catch (e) {
Utils.logger.error(`Error inside handleTokenInvalidation* ()`);
Utils.logger.error(e);
}
}

function* auth() {
try {
yield;
} catch (e) {
Utils.logger.error("Error inside auth* ()");
Utils.logger.error(e);
}
}

function* nodeSaga() {
yield all([
auth,
takeEvery("node/tokenDidInvalidate", handleTokenInvalidation)
]);
}

export default nodeSaga;
Loading

0 comments on commit b5ecefa

Please sign in to comment.