ソースを参照

Merge pull request #27 from porter-dev/login-integration

Login integration
jusrhee 5 年 前
コミット
5b86b5d97e

+ 3 - 1
.gitignore

@@ -1 +1,3 @@
-.DS_Store
+.DS_Store
+.env
+app

ファイルの差分が大きいため隠しています
+ 7849 - 2411
dashboard/package-lock.json


+ 16 - 1
dashboard/package.json

@@ -2,7 +2,19 @@
   "name": "dashboard",
   "version": "0.1.0",
   "private": true,
+  "proxy": "http://localhost:5000",
   "dependencies": {
+    "@testing-library/jest-dom": "^4.2.4",
+    "@testing-library/react": "^9.3.2",
+    "@testing-library/user-event": "^7.1.2",
+    "@types/jest": "^24.0.0",
+    "@types/node": "^12.12.62",
+    "@types/react": "^16.9.49",
+    "@types/react-dom": "^16.9.8",
+    "@types/react-modal": "^3.10.6",
+    "@types/react-router": "^5.1.8",
+    "@types/react-router-dom": "^5.1.5",
+    "@types/styled-components": "^5.1.3",
     "ace-builds": "^1.4.12",
     "axios": "^0.20.0",
     "dotenv": "^8.2.0",
@@ -10,7 +22,10 @@
     "react-ace": "^9.1.3",
     "react-dom": "^16.13.1",
     "react-modal": "^3.11.2",
-    "styled-components": "^5.2.0"
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "3.4.3",
+    "styled-components": "^5.2.0",
+    "typescript": "~3.7.2"
   },
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",

+ 25 - 0
dashboard/server.js

@@ -0,0 +1,25 @@
+var express = require('express')
+var path = require('path');
+var bodyParser = require('body-parser')
+
+const app = express();
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+app.use(express.static(path.join(__dirname, 'build')))
+
+// app.get('/auth/check', (req, res) => {
+//     if (req.cookie) {
+//         return true
+//     } else {
+//         return false
+//     }
+// })
+
+app.get('/*', (req, res) => {
+    res.sendFile(path.join(__dirname, 'build', 'index.html'))
+})
+
+app.listen(5000, () => {
+    console.log('ok')
+})

+ 1 - 20
dashboard/src/App.tsx

@@ -8,9 +8,6 @@ type PropsType = {
 };
 
 type StateType = {
-  isLoading: boolean,
-  isLoggedIn: boolean
-  uninitialized: boolean,
 };
 
 export default class App extends Component<PropsType, StateType> {
@@ -21,20 +18,4 @@ export default class App extends Component<PropsType, StateType> {
       </ContextProvider>
     );
   }
-}
-
-const GlobalStyle = createGlobalStyle`
-  * {
-    box-sizing: border-box;
-  }
-`;
-
-const StyledApp = styled.div`
-  height: 100vh;
-  width: 100vw;
-  position: fixed;
-  top: 0;
-  left: 0;
-  background: #24272a;
-  color: white;
-`;
+}

+ 2 - 5
dashboard/src/main/Login.tsx

@@ -34,15 +34,12 @@ export default class Login extends Component<PropsType, StateType> {
     if (!emailRegex.test(email)) {
       this.setState({ emailError: true });
     } else {
-      
       // Attempt user login
-      api.logInUser('<token>', {
+      api.logInUser('', {
         email: email,
         password: password
       }, {}, (err: any, res: any) => {
         // TODO: case and set credential error
-
-        console.log(err)
         err ? setCurrentError(JSON.stringify(err)) : authenticate();
       });
     }
@@ -65,7 +62,7 @@ export default class Login extends Component<PropsType, StateType> {
       );
     }
   }
-
+  
   render() {
     let { email, password, credentialError, emailError } = this.state;
 

+ 57 - 22
dashboard/src/main/Main.tsx

@@ -2,48 +2,46 @@ import React, { Component } from 'react';
 import styled, { createGlobalStyle } from 'styled-components';
 import close from '../assets/close.png';
 
+import api from '../shared/api';
 import { Context } from '../shared/Context';
 
 import Login from './Login';
 import Register from './Register';
 import Home from './home/Home';
+import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
 
 type PropsType = {
 };
 
 type StateType = {
   isLoading: boolean,
-  isLoggedIn: boolean
-  uninitialized: boolean,
+  isLoggedIn: boolean,
+  initialized: boolean,
 };
 
 export default class Main extends Component<PropsType, StateType> {
+  
   state = {
     isLoading: false,
-    isLoggedIn: true,
-    uninitialized: true,
-  };
+    isLoggedIn : false,
+    initialized: false
+  }
 
   componentDidMount() {
-    // Check if Porter has already been initialized
-    if (false) {
-      // Check if user is logged in
-      if (false) {
-        this.setState({ isLoggedIn: true });
+    localStorage.getItem("init") == 'true' ? this.setState({initialized: true}) : this.setState({initialized: false})
+    api.checkAuth('', {}, {}, (err: any, res: any) => {
+      if (res.data) {
+        this.setState({ isLoggedIn: true, initialized: true})
+      } else {
+        this.setState({ isLoggedIn: false })
       }
-    }
+    });
   }
 
-  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 })} />
-  };
+  initialize = () => {
+    this.setState({isLoggedIn: true, initialized: true});
+    localStorage.setItem('init', 'true');
+  }
 
   renderCurrentError = (): JSX.Element | undefined => {
     if (this.context.currentError) {
@@ -60,9 +58,46 @@ export default class Main extends Component<PropsType, StateType> {
 
   render() {
     return (
+
       <StyledMain>
         <GlobalStyle />
-        {this.renderContents()}
+        <BrowserRouter>
+          <Switch>
+            <Route path='/login' render={() => {
+              if (!this.state.isLoggedIn && this.state.initialized) {
+                return <Login authenticate={() => this.setState({ isLoggedIn: true, initialized: true })} />
+              } else {
+                return <Redirect to='/' />
+              }
+            }} />
+
+            <Route path='/register' render={() => {
+              if (!this.state.initialized) {
+                return <Register authenticate={this.initialize} />
+              } else {
+                return <Redirect to='/' />
+              }
+            }} />
+
+            <Route path='/dashboard' render={() => {
+              if (this.state.isLoggedIn && this.state.initialized) {
+                return <Home logOut={() => this.setState({ isLoggedIn: false, initialized: true })} />
+              } else {
+                return <Redirect to='/' />
+              }
+            }}/>
+
+            <Route path='/' render={() => {
+              if (this.state.isLoggedIn) {
+                return <Redirect to='/dashboard'/>
+              } else if (this.state.initialized) {
+                return <Redirect to='/login'/>
+              } else {
+                return <Redirect to='/register' />
+              }
+            }}/>
+          </Switch>
+        </BrowserRouter>
         {this.renderCurrentError()}
       </StyledMain>
     );

+ 1 - 0
dashboard/src/main/Register.tsx

@@ -48,6 +48,7 @@ export default class Register extends Component<PropsType, StateType> {
         email: email,
         password: password
       }, {}, (err: any, res: any) => {
+        console.log('err',err)
         err ? setCurrentError(JSON.stringify(err)) : authenticate();
       });
     } 

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

@@ -30,6 +30,7 @@ class ContextProvider extends Component {
     },
     currentError: null as string | null,
     setCurrentError: (currentError: string): void => {
+      console.log('setting err', currentError)
       this.setState({ currentError });
     },
     currentCluster: null as string | null,

+ 5 - 2
dashboard/src/shared/api.tsx

@@ -9,17 +9,19 @@ import { baseApi } from './baseApi';
  * @param {(err: Object, res: Object) => void} callback - Callback function.
  */
 
+const checkAuth = baseApi('GET', '/api/auth/check');
+
 const registerUser = baseApi<{ 
   email: string, 
   password: string
-}>('POST', '/api/register');
+}>('POST', '/api/users');
 
 const logInUser = baseApi<{
   email: string,
   password: string
 }>('POST', '/api/login');
 
-const logOutUser = baseApi<{}>('GET', '/api/logout');
+const logOutUser = baseApi('POST', '/api/logout');
 
 const getUser = baseApi<{}, { id: number }>('GET', pathParams => {
   return `/api/users/${pathParams.id}`;
@@ -42,6 +44,7 @@ const getAllClusters = baseApi<{}, { id: number }>('GET', pathParams => {
 
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
+  checkAuth,
   registerUser,
   logInUser,
   logOutUser,

+ 6 - 8
dashboard/src/shared/baseApi.tsx

@@ -18,8 +18,8 @@ export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((p
 
     // Handle request type (can refactor)
     if (requestType === 'POST') {
-      axios.post(`https://${baseUrl + endpointString}`, params, {
-        headers: {
+      axios.post(`http://${baseUrl + endpointString}`, params, {
+      headers: {
           Authorization: `Bearer ${token}`
         }
       })
@@ -30,7 +30,7 @@ export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((p
         callback && callback(err, null);
       });
     } else if (requestType === 'PUT') {
-      axios.put(`https://${baseUrl + endpointString}`, params, {
+      axios.put(`http://${baseUrl + endpointString}`, params, {
         headers: {
           Authorization: `Bearer ${token}`
         }
@@ -42,14 +42,12 @@ export const baseApi = <T extends {}, S = {}>(requestType: string, endpoint: ((p
         callback && callback(err, null);
       });
     } else {
-      axios.get(`https://${baseUrl + endpoint}`, {
-        headers: {
-          Authorization: `Bearer ${token}`
-        },
+      axios.get(`http://${baseUrl + endpointString}`, {
+        withCredentials: true,
         params
       })
       .then(res => {
-        callback && callback(null, res.data);
+        callback && callback(null, res);
       })
       .catch(err => {
         callback && callback(err, null);

+ 4 - 0
dashboard/webpack.config.js

@@ -52,6 +52,10 @@ module.exports = () => {
     output: {
       filename: 'bundle.js',
       path: path.resolve(__dirname, 'build'),
+      publicPath: '/'
+    },
+    devServer: {
+      historyApiFallback: true,
     },
     plugins: [
       new HtmlWebpackPlugin({

+ 19 - 3
docker-compose.yaml

@@ -24,6 +24,22 @@ services:
     volumes:
       - db:/var/lib/postgresql/data
 
-volumes:
-  db:
-  metabase:
+  # metabase:
+  #   image: metabase/metabase
+  #   restart: always
+  #   ports: 
+  #     - 3000:3000
+  #   volumes: 
+  #     - metabase:/metabase-data
+  #   environment:
+  #     MB_DB_TYPE: postgres
+  #     MB_DB_DBNAME: porter
+  #     MB_DB_PORT: 5432
+  #     MB_DB_USER: porter
+  #     MB_DB_PASS: porter
+  #     MB_DB_HOST: postgres
+  #   depends_on:
+  #     - postgres
+  #   volumes:
+  #     db:
+  #     metabase:

+ 2 - 1
docker/.env

@@ -9,4 +9,5 @@ DB_HOST=postgres
 DB_PORT=5432
 DB_USER=porter
 DB_PASS=porter
-DB_NAME=porter
+DB_NAME=porter
+COOKIE_SECRETS=secret

+ 1 - 0
go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/creack/pty v1.1.11 // indirect
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/go-chi/chi v4.1.2+incompatible
+	github.com/go-chi/cors v1.1.1
 	github.com/go-playground/locales v0.13.0
 	github.com/go-playground/universal-translator v0.17.0
 	github.com/go-playground/validator/v10 v10.3.0

+ 2 - 0
go.sum

@@ -90,6 +90,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H
 github.com/go-chi/chi v1.0.0 h1:s/kv1cTXfivYjdKJdyUzNGyAWZ/2t7duW1gKn5ivu+c=
 github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
 github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/cors v1.1.1 h1:eHuqxsIw89iXcWnWUN8R72JMibABJTN/4IOYI5WERvw=
+github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=

+ 3 - 1
internal/auth/sessionstore.go

@@ -111,7 +111,9 @@ func (store *PGStore) save(session *sessions.Session) error {
 
 // NewStore takes an initialized db and session key pairs to create a session-store in postgres db.
 func NewStore(repo *repository.Repository, conf config.ServerConf) (*PGStore, error) {
-	keyPairs := conf.CookieSecrets
+	keyPairs := [][]byte{
+		conf.CookieSecret,
+	}
 
 	dbStore := &PGStore{
 		Codecs: securecookie.CodecsFromPairs(keyPairs...),

+ 6 - 6
internal/config/config.go

@@ -16,12 +16,12 @@ type Conf struct {
 
 // ServerConf is the server configuration
 type ServerConf struct {
-	Port          int           `env:"SERVER_PORT,default=8080"`
-	CookieName    string        `env:"COOKIE_NAME,default=porter"`
-	CookieSecrets [][]byte      `env:"COOKIE_SECRETS,default=secret"`
-	TimeoutRead   time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
-	TimeoutWrite  time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
-	TimeoutIdle   time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
+	Port         int           `env:"SERVER_PORT,default=8080"`
+	CookieName   string        `env:"COOKIE_NAME,default=porter"`
+	CookieSecret []byte        `env:"COOKIE_SECRETS,default=secret"`
+	TimeoutRead  time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
+	TimeoutWrite time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
+	TimeoutIdle  time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 25 - 3
server/api/user_handler.go

@@ -3,11 +3,13 @@ package api
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
+	"golang.org/x/crypto/bcrypt"
 
 	"gorm.io/gorm"
 
@@ -15,7 +17,6 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
-	"golang.org/x/crypto/bcrypt"
 )
 
 // Enumeration of user API error codes, represented as int64
@@ -53,6 +54,25 @@ func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
+// HandleAuthCheck checks whether current session is authenticated.
+func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
+	session, err := app.store.Get(r, app.cookieName)
+	cook, _ := r.Cookie("porter")
+	fmt.Println("cooki", cook)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+
+	if auth, ok := session.Values["authenticated"].(bool); !auth || !ok {
+		app.logger.Info().Msgf(strconv.FormatBool(auth))
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte("false"))
+		return
+	}
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte("true"))
+}
+
 // HandleLoginUser checks the request header for cookie and validates the user.
 func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 	session, err := app.store.Get(r, app.cookieName)
@@ -62,7 +82,6 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 	}
 
 	form := &forms.LoginUserForm{}
-
 	// decode from JSON to form value
 	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
@@ -92,7 +111,10 @@ func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
 	// Set user as authenticated
 	session.Values["authenticated"] = true
 	session.Values["user_id"] = storedUser.ID
-	session.Save(r, w)
+	if err := session.Save(r, w); err != nil {
+		app.logger.Warn().Err(err)
+	}
+
 	w.WriteHeader(http.StatusOK)
 }
 

+ 1 - 0
server/router/router.go

@@ -26,6 +26,7 @@ func New(a *api.App, store *sessionstore.PGStore, cookieName string) *chi.Mux {
 		r.Method("PUT", "/users/{id}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleUpdateUser, l)))
 		r.Method("DELETE", "/users/{id}", auth.DoesUserIDMatch(requestlog.NewHandler(a.HandleDeleteUser, l)))
 		r.Method("POST", "/login", requestlog.NewHandler(a.HandleLoginUser, l))
+		r.Method("GET", "/auth/check", requestlog.NewHandler(a.HandleAuthCheck, l))
 		r.Method("POST", "/logout", auth.BasicAuthenticate(requestlog.NewHandler(a.HandleLogoutUser, l)))
 	})
 

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません