diff --git a/http/handler.go b/http/handler.go index 0852242aa..9ebdadc9b 100644 --- a/http/handler.go +++ b/http/handler.go @@ -363,7 +363,7 @@ func (h *Handler) collectStats(next http.Handler) http.Handler { // latticeRoutes lists the frontend routes that do not directly correspond to // backend routes, and require special handling. -var latticeRoutes = []string{"/tables", "/query", "/querybuilder", "/login"} // TODO somehow pull this from some metadata in the lattice directory +var latticeRoutes = []string{"/tables", "/query", "/querybuilder", "/signin"} // TODO somehow pull this from some metadata in the lattice directory // newRouter creates a new mux http router. func newRouter(handler *Handler) http.Handler { diff --git a/lattice/src/App.tsx b/lattice/src/App.tsx index bf55e0ace..8481007b9 100644 --- a/lattice/src/App.tsx +++ b/lattice/src/App.tsx @@ -1,60 +1,37 @@ -import React, { useEffect, useState } from 'react'; -import CssBaseline from '@material-ui/core/CssBaseline'; -import { Route, Switch } from 'react-router-dom'; -import { darkTheme, lightTheme } from 'theme/'; -import { Home } from 'App/Home'; -import { Header } from 'shared/Header'; +import Login from 'App/AuthFlow/Login'; +import Main from 'Main'; +import { BrowserRouter, Route, Switch } from 'react-router-dom'; +import { useAuth } from 'services/useAuth'; +import PrivateRoute from 'shared/PrivateRoute/PrivateRoute'; +import { lightTheme } from 'theme/'; + import { MuiThemeProvider } from '@material-ui/core/styles'; -import { Nav } from 'shared/Nav'; -import { NotFound } from 'App/NotFound'; -import { MoleculaTablesContainer } from 'App/MoleculaTables'; -import { QueryContainer } from 'App/Query'; -import { QueryBuilderContainer } from 'App/QueryBuilder'; -import css from './App.module.scss'; -import Login from 'App/Login/Login'; const App = () => { - const [theme, setTheme] = useState( - localStorage.getItem('theme') || 'light' - ); - - useEffect(() => { - if(theme === 'dark') { - document.documentElement.setAttribute('data-theme', 'dark') - } else { - document.documentElement.removeAttribute('data-theme'); - } - }, [theme]); - - const onToggleTheme = () => { - const newTheme = theme === 'dark' ? 'light' : 'dark'; - setTheme(newTheme); - localStorage.setItem('theme', newTheme); - }; + const auth = useAuth(); return ( - - -
- -
-
-
-
- + ) : ( + + )} + + )} + ); -} +}; export default App; diff --git a/lattice/src/App/AuthFlow/AuthFlow.module.scss b/lattice/src/App/AuthFlow/AuthFlow.module.scss new file mode 100644 index 000000000..5ac275622 --- /dev/null +++ b/lattice/src/App/AuthFlow/AuthFlow.module.scss @@ -0,0 +1,56 @@ +.main { + min-height: 100vh; + background-repeat: no-repeat; + background-image: linear-gradient( + to bottom, + rgba(250, 250, 250, 1), + rgba(250, 250, 250, 0.7) + ), + url(/assets/bg-pattern.png); + background-size: cover; + padding-bottom: 32px; +} + +.logoContainer { + text-align: center; +} + +.logo { + height: 85px; + margin: 16px; +} + +.loginForm { + width: 500px; + margin: 0 auto; + padding-top: 75px; +} + +.formError { + color: #f44336; + margin-bottom: 16px; +} + +.sso { + text-align: center; + padding: 24px 0 16px; +} + +.passwordField { + position: relative; + + .forgotPassword { + // [syang] Eww yes, I hate this + position: absolute; + right: 0; + z-index: 1; + } +} + +.backToSignIn { + padding: 24px 0 16px; +} + +.alert { + margin-bottom: 16px; +} diff --git a/lattice/src/App/AuthFlow/Login.tsx b/lattice/src/App/AuthFlow/Login.tsx new file mode 100644 index 000000000..a050e7ebf --- /dev/null +++ b/lattice/src/App/AuthFlow/Login.tsx @@ -0,0 +1,34 @@ +import { ReactComponent as MLogo } from 'assets/m-bug-alt.svg'; + +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; + +import css from './AuthFlow.module.scss'; +import SignInButton from './SignInButton'; + +function Login(props) { + const renderLoginForm = () => ( + + + + + + + ); + + return ( +
+
+
+ +
+ {renderLoginForm()} +
+
+ ); +} +export default Login; diff --git a/lattice/src/App/AuthFlow/SignInButton.tsx b/lattice/src/App/AuthFlow/SignInButton.tsx new file mode 100644 index 000000000..b46ea8ba0 --- /dev/null +++ b/lattice/src/App/AuthFlow/SignInButton.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { Button } from '@material-ui/core'; + +interface Props { + children?: React.ReactNode; +} + +const SignInButton: React.FC = ({ children }) => { + return ( + + + + ); +}; + +export default SignInButton; diff --git a/lattice/src/App/AuthFlow/SignOutButton.tsx b/lattice/src/App/AuthFlow/SignOutButton.tsx new file mode 100644 index 000000000..7ff19b99f --- /dev/null +++ b/lattice/src/App/AuthFlow/SignOutButton.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { Button } from '@material-ui/core'; + +interface Props { + children?: React.ReactNode; +} + +const SignOutButton: React.FC = ({ children }) => { + return ( + + + + ); +}; + +export default SignOutButton; diff --git a/lattice/src/App/AuthFlow/index.ts b/lattice/src/App/AuthFlow/index.ts new file mode 100644 index 000000000..f1d32a23a --- /dev/null +++ b/lattice/src/App/AuthFlow/index.ts @@ -0,0 +1 @@ +export * from './Login'; \ No newline at end of file diff --git a/lattice/src/App/Login/Login.tsx b/lattice/src/App/Login/Login.tsx deleted file mode 100644 index ebd4a1f7b..000000000 --- a/lattice/src/App/Login/Login.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import LoginButton from './LoginButton'; -import { pilosa } from 'services/eventServices'; - -function login() { - pilosa.get.login().then((res) => { - console.log(`login result:`, res); - }); -} - -function Login() { - return ( - <> -
Login
- - - ); -} - -export default Login; diff --git a/lattice/src/App/Login/LoginButton.tsx b/lattice/src/App/Login/LoginButton.tsx deleted file mode 100644 index 88b09634a..000000000 --- a/lattice/src/App/Login/LoginButton.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -interface Props { - onClick: () => void; -} - -const LoginButton: React.FC = ({ onClick }) => { - return ; -}; - -export default LoginButton; diff --git a/lattice/src/App/Login/index.ts b/lattice/src/App/Login/index.ts deleted file mode 100644 index a10c3a83a..000000000 --- a/lattice/src/App/Login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Login'; diff --git a/lattice/src/Main.tsx b/lattice/src/Main.tsx new file mode 100644 index 000000000..bfa1946ee --- /dev/null +++ b/lattice/src/Main.tsx @@ -0,0 +1,67 @@ +import { Home } from "App/Home"; +import { MoleculaTablesContainer } from "App/MoleculaTables"; +import { NotFound } from "App/NotFound"; +import { QueryContainer } from "App/Query"; +import { QueryBuilderContainer } from "App/QueryBuilder"; +import { useEffect, useState } from "react"; +import { Route, Switch } from "react-router-dom"; +import { Header } from "shared/Header"; +import { Nav } from "shared/Nav"; +import { darkTheme, lightTheme } from "theme/"; + +import CssBaseline from "@material-ui/core/CssBaseline"; +import { MuiThemeProvider } from "@material-ui/core/styles"; + +import css from "./App.module.scss"; + +const Main = () => { + const [theme, setTheme] = useState( + localStorage.getItem("theme") || "light" + ); + + useEffect(() => { + if (theme === "dark") { + document.documentElement.setAttribute("data-theme", "dark"); + } else { + document.documentElement.removeAttribute("data-theme"); + } + }, [theme]); + + const onToggleTheme = () => { + const newTheme = theme === "dark" ? "light" : "dark"; + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + }; + + return ( +
+ + +
+
+
+
+
+ +
+ ); +}; + +export default Main; diff --git a/lattice/src/assets/bg-pattern.png b/lattice/src/assets/bg-pattern.png new file mode 100644 index 000000000..23bdf09be Binary files /dev/null and b/lattice/src/assets/bg-pattern.png differ diff --git a/lattice/src/assets/m-bug-alt.svg b/lattice/src/assets/m-bug-alt.svg new file mode 100644 index 000000000..a0cc81bc1 --- /dev/null +++ b/lattice/src/assets/m-bug-alt.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/lattice/src/index.tsx b/lattice/src/index.tsx index 331a583a3..b8b53d0af 100644 --- a/lattice/src/index.tsx +++ b/lattice/src/index.tsx @@ -1,15 +1,19 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; -import { BrowserRouter as Router, Route } from 'react-router-dom'; -import * as serviceWorker from './serviceWorker'; import './index.scss'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { ProvideAuth } from 'services/useAuth'; + +import App from './App'; +import * as serviceWorker from './serviceWorker'; + ReactDOM.render( - - - , - document.getElementById('root') + + + + + , + document.getElementById("root") ); // If you want your app to work offline and load faster, you can change diff --git a/lattice/src/services/eventServices.tsx b/lattice/src/services/eventServices.tsx index 722ebc59f..2fdca224d 100644 --- a/lattice/src/services/eventServices.tsx +++ b/lattice/src/services/eventServices.tsx @@ -1,49 +1,53 @@ import axios from 'axios'; + import { baseURL } from './baseURL'; const api = axios.create({ baseURL, headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json' - } + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, }); export const pilosa = { get: { status() { - return api.get('/status'); + return api.get("/status"); }, - login() { - return api.get('/login'); + auth() { + return api.get("/auth"); + }, + userinfo() { + return api.get("/userinfo"); }, info() { - return api.get('/info'); + return api.get("/info"); }, version() { - return api.get('/version'); + return api.get("/version"); }, transactions() { - return api.get('/ui/transaction'); + return api.get("/ui/transaction"); }, transaction(id) { return api.get(`/transaction/${id}`); }, schema() { - return api.get('/schema'); + return api.get("/schema"); }, schemaDetails() { - return api.get('/schema/details'); + return api.get("/schema/details"); }, metrics() { - return api.get('/metrics.json'); + return api.get("/metrics.json"); }, usage() { - return api.get('/ui/usage'); + return api.get("/ui/usage"); }, queryHistory() { - return api.get('/query-history'); - } + return api.get("/query-history"); + }, }, post: { finishTransaction(id) { @@ -51,6 +55,6 @@ export const pilosa = { }, query(index, query) { return api.post(`/index/${index}/query`, query); - } - } + }, + }, }; diff --git a/lattice/src/services/useAuth.tsx b/lattice/src/services/useAuth.tsx new file mode 100644 index 000000000..d6f5c7421 --- /dev/null +++ b/lattice/src/services/useAuth.tsx @@ -0,0 +1,95 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { pilosa } from './eventServices'; + +const authContext = createContext({}); + +// Provider component that wraps your app and makes auth object ... +// ... available to any child component that calls useAuth(). +export function ProvideAuth({ children }) { + const auth = useProvideAuth(); + return {children}; +} + +// Hook for child components to get the auth object ... +// ... and re-render when it changes. +export const useAuth = () => { + return useContext(authContext); +}; + +export interface IUser { + userid: string; + username: string; +} + +// Provider hook that creates auth object and handles state +function useProvideAuth() { + const history = useHistory(); + const [user, setUser] = useState(undefined); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [authOn, setAuthOn] = useState(true); + + const userinfo = () => { + pilosa.get.userinfo().then((userinfoRes) => { + if (userinfoRes.data.userid && userinfoRes.data.username) { + setUser(userinfoRes.data); + } else { + setUser(undefined); + } + }); + }; + + const signin = () => { + history.push(`/login`); + }; + + const signout = () => { + history.push("/logout"); + }; + // Subscribe to user on mount + // Because this sets state in the callback it will cause any ... + // ... component that utilizes this hook to re-render with the ... + // ... latest auth object. + useEffect(() => { + pilosa.get + .auth() + .then((res) => { + // User is authenticated + if (res.data === "OK") { + setAuthOn(true); + setIsAuthenticated(true); + + // get userinfo + userinfo(); + } + // Auth is off + else if ( + res.data.startsWith( + "Trying to authenticate but authentication is off" + ) + ) { + setAuthOn(false); + } + // User not authenticated + else { + setAuthOn(true); + setIsAuthenticated(false); + } + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + return { + isAuthenticated, + isLoading, + user, + authOn, + userinfo, + signin, + signout, + }; +} diff --git a/lattice/src/shared/Header/Header.tsx b/lattice/src/shared/Header/Header.tsx index ed2c51bc4..b32e72e63 100644 --- a/lattice/src/shared/Header/Header.tsx +++ b/lattice/src/shared/Header/Header.tsx @@ -1,12 +1,17 @@ -import React, { FC } from 'react'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import { Link } from 'react-router-dom'; -import { ReactComponent as MoleculaLogo } from 'assets/lightTheme/MoleculaLogo.svg'; -import { ReactComponent as MoleculaLogoDark } from 'assets/darkTheme/MoleculaLogo.svg'; -import { ThemeToggle } from 'shared/ThemeToggle'; -import { useTheme } from '@material-ui/core/styles'; -import css from './Header.module.scss'; +import SignOutButton from "App/AuthFlow/SignOutButton"; +import { ReactComponent as MoleculaLogoDark } from "assets/darkTheme/MoleculaLogo.svg"; +import { ReactComponent as MoleculaLogo } from "assets/lightTheme/MoleculaLogo.svg"; +import { FC } from "react"; +import { Link } from "react-router-dom"; +import { useAuth } from "services/useAuth"; +import { ThemeToggle } from "shared/ThemeToggle"; + +import AppBar from "@material-ui/core/AppBar"; +import Button from "@material-ui/core/Button"; +import { useTheme } from "@material-ui/core/styles"; +import Toolbar from "@material-ui/core/Toolbar"; + +import css from "./Header.module.scss"; type HeaderProps = { onToggleTheme: () => void; @@ -14,7 +19,8 @@ type HeaderProps = { export const Header: FC = ({ onToggleTheme }) => { const theme = useTheme(); - const isDark = theme.palette.type === 'dark'; + const isDark = theme.palette.type === "dark"; + const auth = useAuth(); return ( = ({ onToggleTheme }) => { /> + + {auth.isAuthenticated ? ( +
+ {auth.user && ( + + )} + +
+ ) : null}
diff --git a/lattice/src/shared/Nav/Nav.tsx b/lattice/src/shared/Nav/Nav.tsx index 52bfeb1dd..e6d718420 100644 --- a/lattice/src/shared/Nav/Nav.tsx +++ b/lattice/src/shared/Nav/Nav.tsx @@ -51,13 +51,6 @@ export const Nav = () => { - - - - Login - - - ); diff --git a/lattice/src/shared/PrivateRoute/PrivateRoute.tsx b/lattice/src/shared/PrivateRoute/PrivateRoute.tsx new file mode 100644 index 000000000..16fe98247 --- /dev/null +++ b/lattice/src/shared/PrivateRoute/PrivateRoute.tsx @@ -0,0 +1,33 @@ +import { Redirect, Route } from 'react-router-dom'; +import { useAuth } from 'services/useAuth'; + +function PrivateRoute({ component: Component, ...rest }) { + const auth = useAuth(); + + return ( + { + // If the user is authed render the component + if (auth.isAuthenticated) { + // if (true) { + return ; + } else { + // If they are not then we need to redirect to a public page + return ( + + ); + } + }} + /> + ); +} + +export default PrivateRoute; diff --git a/lattice/src/theme/darkTheme.tsx b/lattice/src/theme/darkTheme.tsx index 5f8492608..3e48e31c5 100644 --- a/lattice/src/theme/darkTheme.tsx +++ b/lattice/src/theme/darkTheme.tsx @@ -1,8 +1,8 @@ /* tslint:disable */ -import { createMuiTheme } from '@material-ui/core/styles'; +import { createTheme } from '@material-ui/core/styles'; import { baseTheme } from 'theme/'; -export const darkTheme = createMuiTheme({ +export const darkTheme = createTheme({ ...baseTheme, palette: { background: { diff --git a/lattice/src/theme/lightTheme.tsx b/lattice/src/theme/lightTheme.tsx index 3d2e5bc87..fe05fc08f 100644 --- a/lattice/src/theme/lightTheme.tsx +++ b/lattice/src/theme/lightTheme.tsx @@ -1,8 +1,8 @@ /* tslint:disable */ -import { createMuiTheme } from '@material-ui/core/styles'; +import { createTheme } from '@material-ui/core/styles'; import { baseTheme } from 'theme/'; -export const lightTheme = createMuiTheme({ +export const lightTheme = createTheme({ ...baseTheme, palette: { background: {