Просмотр исходного кода

auth flow integrated w/ error handling + baseApi

jusrhee 5 лет назад
Родитель
Сommit
d54eeb29a5

Разница между файлами не показана из-за своего большого размера
+ 80 - 719
dashboard/package-lock.json


+ 3 - 1
dashboard/package.json

@@ -13,6 +13,7 @@
     "@types/react-modal": "^3.10.6",
     "@types/styled-components": "^5.1.3",
     "ace-builds": "^1.4.12",
+    "axios": "^0.20.0",
     "react": "^16.13.1",
     "react-ace": "^9.1.3",
     "react-dom": "^16.13.1",
@@ -41,5 +42,6 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  }
+  },
+  "devDependencies": {}
 }

+ 3 - 25
dashboard/src/App.tsx

@@ -1,10 +1,8 @@
 import React, { Component } from 'react';
 import styled, { createGlobalStyle } from 'styled-components';
 
-import { ContextProvider } from './Context';
-import Login from './main/Login';
-import Register from './main/Register';
-import Home from './main/home/Home';
+import { ContextProvider } from './shared/Context';
+import Main from './main/Main';
 
 type PropsType = {
 };
@@ -16,30 +14,10 @@ type StateType = {
 };
 
 export default class App extends Component<PropsType, StateType> {
-  state = {
-    isLoading: false,
-    isLoggedIn: false,
-    uninitialized: true,
-  };
-
-  renderContents = (): JSX.Element => {
-    if (this.state.isLoading) {
-      return <h1>Loading...</h1>
-    } else if (this.state.isLoggedIn) {
-      return <Home logOut={() => this.setState({ isLoggedIn: false })} />
-    } else if (this.state.uninitialized) {
-      return <Register authenticate={() => this.setState({ isLoggedIn: true })} />
-    }
-    return <Login authenticate={() => this.setState({ isLoggedIn: true })} />
-  };
-
   render() {
     return (
       <ContextProvider>
-        <StyledApp>
-          <GlobalStyle />
-          {this.renderContents()}
-        </StyledApp>
+        <Main />
       </ContextProvider>
     );
   }

+ 0 - 0
dashboard/src/templates/Boilerplate.tsx → dashboard/src/components/Boilerplate.tsx


+ 0 - 0
dashboard/src/lib/YamlEditor.tsx → dashboard/src/components/YamlEditor.tsx


+ 0 - 13
dashboard/src/index.css

@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}

+ 0 - 1
dashboard/src/index.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './index.css';
 import App from './App';
 
 ReactDOM.render(

+ 0 - 74
dashboard/src/lib/YamlEditor.jsx

@@ -1,74 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-import AceEditor from 'react-ace';
-
-import 'ace-builds/src-noconflict/mode-yaml';
-import 'ace-builds/src-noconflict/theme-monokai';
-
-class YamlEditor extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      yaml: ``,
-    }
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSubmit = this.handleSubmit.bind(this);
-  }
-
-  // Uses the yaml-lint library to determine if a given string is valid yaml.
-  // If the code is invalid, it returns an error message detailing what went wrong.
-  checkYaml = (y) => {
-    /*
-    yamlLint.lint(y).then(() => {
-      alert('Valid YAML file.');
-    }).catch((error) => {
-      alert(error.message);
-    });
-    */
-  }
-
-  // Calls checkYaml and passes in the value from the textarea
-  handleChange = (e) => {
-    this.setState({ yaml: e });
-  }
-  handleSubmit = (e) => {
-    this.checkYaml(this.state.yaml);
-    e.preventDefault();
-  }
-
-  render() {
-    return (
-      <Holder>
-        <Editor onSubmit={this.handleSubmit}>
-          <AceEditor
-            mode='yaml'
-            theme='monokai'
-            onChange={this.handleChange}
-            name='codeEditor'
-            editorProps={{ $blockScrolling: true }}
-            width='100%'
-            defaultValue={`# If you are using certificate files, include those explicitly`}
-            style={{ 
-              borderRadius: '5px',
-            }}
-          />
-        </Editor>
-      </Holder>
-    );
-  }
-}
-
-export default YamlEditor;
-
-const Editor = styled.form`
-  margin-top: 0px;
-  margin-bottom: 12px;
-  width: calc(100% - 0px);
-  border-radius: 5px;
-  border: 1px solid #ffffff22;
-  height: 295px;
-  overflow: auto;
-`;
-
-const Holder = styled.div`
-`;

+ 140 - 8
dashboard/src/main/Login.tsx

@@ -1,28 +1,115 @@
-import React, { Component } from 'react';
+import React, { ChangeEvent, Component } from 'react';
 import styled from 'styled-components';
-
 import logo from '../assets/logo.png';
 
+import api from '../shared/api';
+import { emailRegex } from '../shared/regex';
+import { Context } from '../shared/Context';
+
 type PropsType = {
   authenticate: () => void
 };
 
 type StateType = {
+  email: string,
+  password: string,
+  emailError: boolean,
+  credentialError: boolean
 };
 
 export default class Login extends Component<PropsType, StateType> {
+  state = {
+    email: '',
+    password: '',
+    emailError: false,
+    credentialError: false
+  }
+
+  handleLogin = (): void => {
+    let { email, password } = this.state;
+    let { authenticate } = this.props;
+    let { setCurrentError } = this.context;
+
+    // Check for valid input
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    } else {
+      
+      // Attempt user login
+      api.logInUser('', {
+        email: email,
+        password: password
+      }, (err, res) => {
+        // TODO: case and set credential error
+        
+        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+      });
+    }
+  }
+
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper><div />Please enter a valid email</ErrorHelper>
+      );
+    }
+  }
+
+  renderCredentialError = () => {
+    let { credentialError } = this.state;
+    if (credentialError) {
+      return (
+        <ErrorHelper><div />Incorrect email or password</ErrorHelper>
+      );
+    }
+  }
+
   render() {
+    let { email, password, credentialError, emailError } = this.state;
+
     return (
       <StyledLogin>
         <LoginPanel>
-          <GradientBg />
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
           <FormWrapper>
             <Logo src={logo} />
             <Line />
             <Prompt>Log in to Porter</Prompt>
-            <Input placeholder='Username' />
-            <Input type='password' placeholder='Password' />
-            <Button onClick={this.props.authenticate}>Continue</Button>
+            <InputWrapper>
+              <Input 
+                type='email' 
+                placeholder='Email'
+                value={email}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    email: e.target.value,
+                    emailError: false,
+                    credentialError: false
+                  })
+                }
+                valid={!credentialError && !emailError}
+              />
+              {this.renderEmailError()}
+            </InputWrapper>
+            <InputWrapper>
+              <Input 
+                type='password' 
+                placeholder='Password'
+                value={password}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    password: e.target.value, 
+                    credentialError: false 
+                  })
+                }
+                valid={!credentialError}
+              />
+              {this.renderCredentialError()}
+            </InputWrapper>
+            <Button onClick={this.handleLogin}>Continue</Button>
           </FormWrapper>
         </LoginPanel>
       </StyledLogin>
@@ -30,6 +117,46 @@ export default class Login extends Component<PropsType, StateType> {
   }
 }
 
+Login.contextType = Context;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
 const Line = styled.div`
   height: 3px;
   width: 100px;
@@ -54,6 +181,10 @@ const Button = styled.button`
   font-size: 14px;
 `;
 
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
 const Input = styled.input`
   width: 200px;
   font-family: 'Work Sans', sans-serif;
@@ -62,7 +193,7 @@ const Input = styled.input`
   padding: 8px;
   background: #ffffff12;
   color: #ffffff;
-  border: 0;
+  border: ${(props: { valid?: boolean }) => props.valid ? '0' : '1px solid #ff3b62'};
   border-radius: 2px;
   font-size: 14px
 `;
@@ -96,6 +227,8 @@ const GradientBg = styled.div`
   width: 180%;
   height: 180%;
   position: absolute;
+  top: -40%;
+  left: -40%;
   animation: flip 6s infinite linear;
   @keyframes flip {
     from { transform: rotate(0deg); }
@@ -109,7 +242,6 @@ const LoginPanel = styled.div`
   background: white;
   margin-top: -20px;
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   justify-content: center;
   position: relative;

+ 147 - 0
dashboard/src/main/Main.tsx

@@ -0,0 +1,147 @@
+import React, { Component } from 'react';
+import styled, { createGlobalStyle } from 'styled-components';
+import close from '../assets/close.png';
+
+import { Context } from '../shared/Context';
+
+import Login from './Login';
+import Register from './Register';
+import Home from './home/Home';
+
+type PropsType = {
+};
+
+type StateType = {
+  isLoading: boolean,
+  isLoggedIn: boolean
+  uninitialized: boolean,
+};
+
+export default class Main extends Component<PropsType, StateType> {
+  state = {
+    isLoading: false,
+    isLoggedIn: true,
+    uninitialized: false,
+  };
+
+  componentDidMount() {
+    // Check if Porter has already been initialized
+    if (false) {
+      // Check if user is logged in
+      if (false) {
+        this.setState({ isLoggedIn: true });
+      }
+    }
+  }
+
+  renderContents = (): JSX.Element => {
+    if (this.state.isLoading) {
+      return <h1>Loading...</h1>
+    } else if (this.state.isLoggedIn) {
+      return <Home logOut={() => this.setState({ isLoggedIn: false })} />
+    } else if (this.state.uninitialized) {
+      return <Register authenticate={() => this.setState({ isLoggedIn: true })} />
+    }
+    return <Login authenticate={() => this.setState({ isLoggedIn: true })} />
+  };
+
+  renderCurrentError = (): JSX.Element | undefined => {
+    if (this.context.currentError) {
+      return (
+        <CurrentError>
+          <ErrorText>Error: {this.context.currentError}</ErrorText>
+          <CloseButton onClick={() => { this.context.setCurrentError(null) }}>
+            <CloseButtonImg src={close} />
+          </CloseButton>
+        </CurrentError>
+      );
+    }
+  }
+
+  render() {
+    return (
+      <StyledMain>
+        <GlobalStyle />
+        {this.renderContents()}
+        {this.renderCurrentError()}
+      </StyledMain>
+    );
+  }
+}
+
+Main.contextType = Context;
+
+const CloseButton = styled.div`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  margin-left: 10px;
+  cursor: pointer;
+  :hover {
+    background-color: #ffffff11;
+  }
+`;
+
+const CloseButtonImg = styled.img`
+  width: 10px;
+`;
+
+const GlobalStyle = createGlobalStyle`
+  * {
+    box-sizing: border-box;
+  }
+`;
+
+const ErrorText = styled.div`
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  width: calc(100% - 50px);
+`;
+
+const CurrentError = styled.div`
+  position: fixed;
+  bottom: 20px;
+  width: 220px;
+  left: 17px;
+  padding: 15px;
+  padding-right: 0px;
+  font-family: 'Work Sans', sans-serif;
+  height: 50px;
+  font-size: 13px;
+  border-radius: 3px;
+  background: #383842dd;
+  border: 1px solid #ffffff55;
+  display: flex;
+  align-items: center;
+
+  > i {
+    font-size: 18px;
+    margin-right: 10px;
+  }
+
+  animation: floatIn 0.5s;
+  animation-fill-mode: forwards;
+
+  @keyframes floatIn {
+    from {
+      opacity: 0; transform: translateY(20px);
+    }
+    to {
+      opacity: 1; transform: translateY(0px);
+    }
+  }
+`;
+
+const StyledMain = styled.div`
+  height: 100vh;
+  width: 100vw;
+  position: fixed;
+  top: 0;
+  left: 0;
+  background: #24272a;
+  color: white;
+`;

+ 155 - 10
dashboard/src/main/Register.tsx

@@ -1,34 +1,134 @@
-import React, { Component } from 'react';
+import React, { ChangeEvent, Component, useContext } from 'react';
 import styled from 'styled-components';
-
 import logo from '../assets/logo.png';
 
+import api from '../shared/api';
+import { emailRegex } from '../shared/regex';
+import { Context } from '../shared/Context';
+
 type PropsType = {
   authenticate: () => void
 };
 
 type StateType = {
+  email: string,
+  password: string,
+  confirmPassword: string,
+  emailError: boolean,
+  confirmPasswordError: boolean
 };
 
 export default class Register extends Component<PropsType, StateType> {
+  state = {
+    email: '',
+    password: '',
+    confirmPassword: '',
+    emailError: false,
+    confirmPasswordError: false
+  }
+
   handleRegister = (): void => {
-    // TODO: Register user
+    let { email, password, confirmPassword } = this.state;
+    let { authenticate } = this.props;
+    let { setCurrentError } = this.context;
+
+    if (!emailRegex.test(email)) {
+      this.setState({ emailError: true });
+    }
 
-    this.props.authenticate();
+    if (confirmPassword !== password) {
+      this.setState({ confirmPasswordError: true });
+    }
+    
+    // Check for valid input
+    if (emailRegex.test(email) && confirmPassword === password) {
+
+      // Attempt user registration
+      api.registerUser('', {
+        email: email,
+        password: password
+      }, (err, res) => {
+        err ? setCurrentError(JSON.stringify(err)) : authenticate();
+      });
+    } 
   };
 
+  renderEmailError = () => {
+    let { emailError } = this.state;
+    if (emailError) {
+      return (
+        <ErrorHelper><div />Please enter a valid email</ErrorHelper>
+      );
+    }
+  }
+
+  renderConfirmPasswordError = () => {
+    let { confirmPasswordError } = this.state;
+    if (confirmPasswordError) {
+      return (
+        <ErrorHelper><div />Password does not match</ErrorHelper>
+      );
+    }
+  }
+
   render() {
+    let { 
+      email,
+      password, 
+      confirmPassword, 
+      emailError,
+      confirmPasswordError
+    } = this.state;
+
     return (
       <StyledRegister>
         <LoginPanel>
-          <GradientBg />
+          <OverflowWrapper>
+            <GradientBg />
+          </OverflowWrapper>
           <FormWrapper>
             <Logo src={logo} />
             <Line />
             <Prompt>Create Account</Prompt>
-            <Input placeholder='Username' />
-            <Input type='password' placeholder='Password' />
-            <Input type='password' placeholder='Confirm Password' />
+            <InputWrapper>
+              <Input 
+                type='email'
+                placeholder='Email'
+                value={email}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ email: e.target.value, emailError: false })
+                }
+                valid={!emailError}
+              />
+              {this.renderEmailError()}
+            </InputWrapper>
+            <Input 
+              type='password' 
+              placeholder='Password'
+              value={password}
+              onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                this.setState({ 
+                  password: e.target.value,
+                  confirmPasswordError: false
+                })
+              }
+              valid={true}
+            />
+            <InputWrapper>
+              <Input 
+                type='password' 
+                placeholder='Confirm Password'
+                value={confirmPassword}
+                onChange={(e: ChangeEvent<HTMLInputElement>) => 
+                  this.setState({ 
+                    confirmPassword: e.target.value, 
+                    confirmPasswordError: false 
+                  })
+                }
+                valid={!confirmPasswordError}
+              />
+              {this.renderConfirmPasswordError()}
+            </InputWrapper>
             <Button onClick={this.handleRegister}>Continue</Button>
           </FormWrapper>
         </LoginPanel>
@@ -37,6 +137,50 @@ export default class Register extends Component<PropsType, StateType> {
   }
 }
 
+Register.contextType = Context;
+
+const OverflowWrapper = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 10px;
+`;
+
+const InputWrapper = styled.div`
+  position: relative;
+`;
+
+const ErrorHelper = styled.div`
+  position: absolute;
+  right: -185px;
+  top: 8px;
+  height: 30px;
+  width: 170px;
+  user-select: none;
+  background: #272731;
+  font-family: 'Work Sans', sans-serif;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ff3b62;
+  border-radius: 3px;
+
+  > div {
+    background: #272731;
+    height: 15px;
+    width: 15px;
+    position: absolute;
+    left: -3px;
+    top: 7px;
+    transform: rotate(45deg);
+    z-index: -1;
+  }
+`;
+
 const Line = styled.div`
   height: 3px;
   width: 100px;
@@ -69,7 +213,7 @@ const Input = styled.input`
   padding: 8px;
   background: #ffffff12;
   color: #ffffff;
-  border: 0;
+  border: ${(props: { valid?: boolean }) => props.valid ? '0' : '1px solid #ff3b62'};
   border-radius: 2px;
   font-size: 14px
 `;
@@ -103,6 +247,8 @@ const GradientBg = styled.div`
   width: 180%;
   height: 180%;
   position: absolute;
+  top: -40%;
+  left: -40%;
   animation: flip 6s infinite linear;
   @keyframes flip {
     from { transform: rotate(0deg); }
@@ -116,7 +262,6 @@ const LoginPanel = styled.div`
   background: white;
   margin-top: -20px;
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   justify-content: center;
   position: relative;

+ 2 - 1
dashboard/src/main/home/Home.tsx

@@ -2,7 +2,8 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import ReactModal from 'react-modal';
 
-import { Context } from '../../Context';
+import { Context } from '../../shared/Context';
+
 import Sidebar from './sidebar/Sidebar';
 import ClusterConfigModal from './modals/ClusterConfigModal';
 

+ 5 - 4
dashboard/src/main/home/modals/ClusterConfigModal.tsx

@@ -3,8 +3,8 @@ import styled from 'styled-components';
 import { textChangeRangeIsUnchanged } from 'typescript';
 import close from '../../../assets/close.png';
 
-import { Context } from '../../../Context';
-import YamlEditor from '../../../lib/YamlEditor';
+import { Context } from '../../../shared/Context';
+import YamlEditor from '../../../components/YamlEditor';
 
 type ClusterOption = {
   name: string,
@@ -36,7 +36,7 @@ export default class ClusterConfigModal extends Component<PropsType, StateType>
   }
 
   renderLine = (tab: string): JSX.Element | undefined => {
-    if (this.state.currentTab == tab) {
+    if (this.state.currentTab === tab) {
       return <Highlight />
     }
   };
@@ -149,7 +149,8 @@ const Row = styled.div`
   display: flex;
   align-items: center;
   color: #ffffff;
-  font-size: 14px;
+  font-size: 13px;
+  font-family: 'Work Sans', sans-serif;
   cursor: pointer;
   
   :hover {

+ 1 - 1
dashboard/src/main/home/sidebar/ClusterSection.tsx

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import drawerBg from '../../../assets/drawer-bg.png';
 
-import { Context } from '../../../Context';
+import { Context } from '../../../shared/Context';
 import Drawer from './Drawer';
 
 type PropsType = {

+ 1 - 1
dashboard/src/main/home/sidebar/Drawer.tsx

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
 import styled from 'styled-components';
 import close from '../../../assets/close.png';
 
-import { Context } from '../../../Context';
+import { Context } from '../../../shared/Context';
 
 type PropsType = {
   showDrawer: boolean,

+ 4 - 0
dashboard/src/Context.tsx → dashboard/src/shared/Context.tsx

@@ -24,6 +24,10 @@ class ContextProvider extends Component {
     currentModal: null,
     setCurrentModal: (currentModal: string): void => {
       this.setState({ currentModal });
+    },
+    currentError: null,
+    setCurrentError: (currentError: string): void => {
+      this.setState({ currentError });
     }
   };
 

+ 20 - 0
dashboard/src/shared/api.tsx

@@ -0,0 +1,20 @@
+import { baseApi } from './baseApi';
+
+const registerUser = baseApi<{ 
+  email: string, 
+  password: string 
+}>('POST', '/api/register');
+
+const logInUser = baseApi<{
+  email: string,
+  password: string
+}>('POST', '/api/login');
+
+const logOutUser = baseApi<{}>('GET', '/api/logout');
+
+// Bundle export to allow default api import
+export default {
+  registerUser,
+  logInUser,
+  logOutUser
+}

+ 35 - 0
dashboard/src/shared/baseApi.tsx

@@ -0,0 +1,35 @@
+import axios from 'axios';
+
+// Partial function that accepts a generic params type and returns an api method
+export const baseApi = <T extends {}>(requestType: string, endpoint: string) => {
+  if (requestType === 'POST') {
+    return (token: string, params?: T, callback?: (err: any, res: any) => void) => {
+      axios.post(`https://${process.env.API_SERVER + endpoint}`, params, {
+        headers: {
+          Authorization: `Bearer ${token}`
+        }
+      })
+      .then(res => {
+        callback && callback(null, res.data);
+      })
+      .catch(err => {
+        callback && callback(err, null);
+      });
+    };
+  }
+
+  return (token: string, params?: T, callback?: (err: any, res: any) => void) => {
+    axios.get(`https://${process.env.API_SERVER + endpoint}`, {
+      headers: {
+        Authorization: `Bearer ${token}`
+      },
+      params
+    })
+    .then(res => {
+      callback && callback(null, res.data);
+    })
+    .catch(err => {
+      callback && callback(err, null);
+    });
+  };
+}

+ 1 - 0
dashboard/src/shared/regex.tsx

@@ -0,0 +1 @@
+export const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

Некоторые файлы не были показаны из-за большого количества измененных файлов