Sfoglia il codice sorgente

Added auth form. Added login and logout actions

Paweł Malak 3 anni fa
parent
commit
e4690d5d9c

+ 1 - 1
client/src/components/Apps/AppForm/AppForm.tsx

@@ -131,7 +131,7 @@ export const AppForm = ({ app, modalHandler }: Props): JSX.Element => {
             onChange={(e) => inputChangeHandler(e)}
           />
           <span>
-            Use icon name from MDI.
+            Use icon name from MDI or pass a valid URL.
             <a href="https://materialdesignicons.com/" target="blank">
               {' '}
               Click here for reference

+ 1 - 1
client/src/components/Bookmarks/Form/BookmarksForm.tsx

@@ -205,7 +205,7 @@ export const BookmarksForm = ({
             onChange={(e) => inputChangeHandler(e)}
           />
           <span>
-            Use icon name from MDI.
+            Use icon name from MDI or pass a valid URL.
             <a href="https://materialdesignicons.com/" target="blank">
               {' '}
               Click here for reference

+ 9 - 3
client/src/components/Settings/AppDetails/AppDetails.module.css

@@ -1,8 +1,14 @@
-.AppVersion {
+.text {
   color: var(--color-primary);
   margin-bottom: 15px;
 }
 
-.AppVersion a {
+.text a,
+.text span {
   color: var(--color-accent);
-}
+}
+
+.separator {
+  margin: 30px 0;
+  border: 1px solid var(--color-primary);
+}

+ 90 - 23
client/src/components/Settings/AppDetails/AppDetails.tsx

@@ -1,33 +1,100 @@
-import { Fragment } from 'react';
+import { FormEvent, Fragment, useState } from 'react';
 
+// Redux
+import { useDispatch, useSelector } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { actionCreators } from '../../../store';
+import { State } from '../../../store/reducers';
+
+// UI
+import { Button, InputGroup, SettingsHeadline } from '../../UI';
+
+// CSS
 import classes from './AppDetails.module.css';
-import { Button } from '../../UI';
+
+// Utils
 import { checkVersion } from '../../../utility';
 
 export const AppDetails = (): JSX.Element => {
+  const { isAuthenticated } = useSelector((state: State) => state.auth);
+
+  const dispatch = useDispatch();
+  const { login, logout } = bindActionCreators(actionCreators, dispatch);
+
+  const [password, setPassword] = useState('');
+
+  const formHandler = (e: FormEvent) => {
+    e.preventDefault();
+    login(password);
+    setPassword('');
+  };
+
   return (
     <Fragment>
-      <p className={classes.AppVersion}>
-        <a
-          href="https://github.com/pawelmalak/flame"
-          target="_blank"
-          rel="noreferrer"
-        >
-          Flame
-        </a>{' '}
-        version {process.env.REACT_APP_VERSION}
-      </p>
-      <p className={classes.AppVersion}>
-        See changelog{' '}
-        <a
-          href="https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md"
-          target="_blank"
-          rel="noreferrer"
-        >
-          here
-        </a>
-      </p>
-      <Button click={() => checkVersion(true)}>Check for updates</Button>
+      <SettingsHeadline text="Authentication" />
+      {!isAuthenticated ? (
+        <form onSubmit={formHandler}>
+          <InputGroup>
+            <label htmlFor="password">Password</label>
+            <input
+              type="password"
+              id="password"
+              name="password"
+              placeholder="••••••"
+              value={password}
+              onChange={(e) => setPassword(e.target.value)}
+            />
+            <span>
+              See
+              <a
+                href="https://github.com/pawelmalak/flame/wiki/Authentication"
+                target="blank"
+              >
+                {` project wiki `}
+              </a>
+              to read more about authentication
+            </span>
+          </InputGroup>
+
+          <Button>Login</Button>
+        </form>
+      ) : (
+        <div>
+          <p className={classes.text}>
+            You are logged in. Your session will expire <span>@@@@</span>
+          </p>
+          <Button click={logout}>Logout</Button>
+        </div>
+      )}
+
+      <hr className={classes.separator} />
+
+      <div>
+        <SettingsHeadline text="App version" />
+        <p className={classes.text}>
+          <a
+            href="https://github.com/pawelmalak/flame"
+            target="_blank"
+            rel="noreferrer"
+          >
+            Flame
+          </a>{' '}
+          version {process.env.REACT_APP_VERSION}
+        </p>
+
+        <p className={classes.text}>
+          See changelog{' '}
+          <a
+            href="https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md"
+            target="_blank"
+            rel="noreferrer"
+          >
+            here
+          </a>
+        </p>
+
+        <Button click={() => checkVersion(true)}>Check for updates</Button>
+      </div>
     </Fragment>
   );
 };

+ 40 - 0
client/src/store/action-creators/auth.ts

@@ -0,0 +1,40 @@
+import { Dispatch } from 'redux';
+import { ApiResponse } from '../../interfaces';
+import { ActionType } from '../action-types';
+import { LoginAction, LogoutAction } from '../actions/auth';
+import axios, { AxiosError } from 'axios';
+
+export const login =
+  (password: string) => async (dispatch: Dispatch<LoginAction>) => {
+    try {
+      const res = await axios.post<ApiResponse<{ token: string }>>(
+        '/api/auth',
+        { password }
+      );
+
+      localStorage.setItem('token', res.data.data.token);
+
+      dispatch({
+        type: ActionType.login,
+        payload: res.data.data.token,
+      });
+    } catch (err) {
+      const apiError = err as AxiosError;
+
+      dispatch<any>({
+        type: ActionType.createNotification,
+        payload: {
+          title: 'Error',
+          message: apiError.response?.data.error,
+        },
+      });
+    }
+  };
+
+export const logout = () => (dispatch: Dispatch<LogoutAction>) => {
+  localStorage.removeItem('token');
+
+  dispatch({
+    type: ActionType.logout,
+  });
+};

+ 1 - 0
client/src/store/action-creators/index.ts

@@ -3,3 +3,4 @@ export * from './config';
 export * from './notification';
 export * from './app';
 export * from './bookmark';
+export * from './auth';

+ 3 - 0
client/src/store/action-types/index.ts

@@ -37,4 +37,7 @@ export enum ActionType {
   addBookmark = 'ADD_BOOKMARK',
   deleteBookmark = 'DELETE_BOOKMARK',
   updateBookmark = 'UPDATE_BOOKMARK',
+  // AUTH
+  login = 'LOGIN',
+  logout = 'LOGOUT',
 }

+ 10 - 0
client/src/store/actions/auth.ts

@@ -0,0 +1,10 @@
+import { ActionType } from '../action-types';
+
+export interface LoginAction {
+  type: ActionType.login;
+  payload: string;
+}
+
+export interface LogoutAction {
+  type: ActionType.logout;
+}

+ 8 - 3
client/src/store/actions/index.ts

@@ -1,3 +1,5 @@
+import { App } from '../../interfaces';
+
 import { SetThemeAction } from './theme';
 
 import {
@@ -24,8 +26,6 @@ import {
   SortAppsAction,
 } from './app';
 
-import { App } from '../../interfaces';
-
 import {
   GetCategoriesAction,
   AddCategoryAction,
@@ -39,6 +39,8 @@ import {
   UpdateBookmarkAction,
 } from './bookmark';
 
+import { LoginAction, LogoutAction } from './auth';
+
 export type Action =
   // Theme
   | SetThemeAction
@@ -71,4 +73,7 @@ export type Action =
   // Bookmarks
   | AddBookmarkAction
   | DeleteBookmarkAction
-  | UpdateBookmarkAction;
+  | UpdateBookmarkAction
+  // Auth
+  | LoginAction
+  | LogoutAction;

+ 34 - 0
client/src/store/reducers/auth.ts

@@ -0,0 +1,34 @@
+import { Action } from '../actions';
+import { ActionType } from '../action-types';
+
+interface AuthState {
+  isAuthenticated: boolean;
+  token: string | null;
+}
+
+const initialState: AuthState = {
+  isAuthenticated: false,
+  token: null,
+};
+
+export const authReducer = (
+  state: AuthState = initialState,
+  action: Action
+): AuthState => {
+  switch (action.type) {
+    case ActionType.login:
+      return {
+        ...state,
+        token: action.payload,
+        isAuthenticated: true,
+      };
+    case ActionType.logout:
+      return {
+        ...state,
+        token: null,
+        isAuthenticated: false,
+      };
+    default:
+      return state;
+  }
+};

+ 2 - 0
client/src/store/reducers/index.ts

@@ -5,6 +5,7 @@ import { configReducer } from './config';
 import { notificationReducer } from './notification';
 import { appsReducer } from './app';
 import { bookmarksReducer } from './bookmark';
+import { authReducer } from './auth';
 
 export const reducers = combineReducers({
   theme: themeReducer,
@@ -12,6 +13,7 @@ export const reducers = combineReducers({
   notification: notificationReducer,
   apps: appsReducer,
   bookmarks: bookmarksReducer,
+  auth: authReducer,
 });
 
 export type State = ReturnType<typeof reducers>;