use ESM, add server setup, cleanup

Change-Id: Iafac35c2082523ae98c31017d9bad5c4d6e18ef3
diff --git a/client/package.json b/client/package.json
index 56ad635..6cd7d4d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,11 +1,13 @@
 {
   "name": "jami-web-client",
+  "type": "module",
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@material-ui/core": "^4.10.2",
-    "@material-ui/icons": "^4.9.1",
-    "@material-ui/lab": "^4.0.0-alpha.56",
+    "@babel/runtime": "^7.13.10",
+    "@material-ui/core": "^4.11.3",
+    "@material-ui/icons": "^4.11.2",
+    "@material-ui/lab": "^4.0.0-alpha.57",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.5.0",
     "@testing-library/user-event": "^7.2.1",
@@ -18,12 +20,15 @@
     "socket.io-client": "^2.3.0"
   },
   "devDependencies": {
+    "@babel/plugin-transform-runtime": "^7.13.15",
     "@babel/core": "^7.13.14",
     "@babel/preset-env": "^7.13.12",
     "@babel/preset-react": "^7.13.13",
+    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-beta.3",
     "babel-loader": "^8.2.2",
     "css-loader": "^5.2.0",
     "html-webpack-plugin": "^5.3.1",
+    "react-refresh": "^0.10.0",
     "sass": "^1.32.8",
     "sass-loader": "^11.0.1",
     "style-loader": "^2.0.0",
diff --git a/client/src/App.js b/client/src/App.js
index 10db90a..cc1b5bf 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -3,52 +3,59 @@
   Author: Larbi Gharib <larbi.gharib@savoirfairelinux.com>
   License: AGPL-3
 */
-
-import React from 'react';
-import CssBaseline from '@material-ui/core/CssBaseline';
+import React, { useState, useEffect } from 'react'
+import { Route, Switch, Redirect, useHistory, useLocation } from 'react-router-dom'
+import { CircularProgress, Container, CssBaseline } from '@material-ui/core'
 import authManager from './AuthManager'
-//import logo from './logo.svg';
-import './App.scss';
-
-import { BrowserRouter as Router, Route, Switch, Link, Redirect } from 'react-router-dom';
+//import logo from './logo.svg'
+import './App.scss'
 
 import SignInPage from "./pages/loginDialog.jsx"
 import JamiMessenger from "./pages/messenger.jsx"
 import AccountSettings from "./pages/accountSettings.jsx"
 import AccountSelection from "./pages/accountSelection.jsx"
-import AddContactPage from "./pages/addContactPage.jsx"
-
+import ServerSetup from "./pages/serverSetup.jsx"
 import NotFoundPage from "./pages/404.jsx"
 
-class App extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      authenticated: authManager.isAuthenticated(),
-    };
-    authManager.setOnAuthChanged(authenticated => this.setState({authenticated}))
-  }
+const App = (props) => {
+    const history = useHistory()
+    const { location } = useLocation()
+    const [state, setState] = useState({
+      loaded: false,
+      auth: authManager.getState()
+    })
+    useEffect(() => {
+      authManager.init(auth => {
+        setState({ loaded: true, auth })
+      })
+      return () => authManager.deinit()
+    }, []);
 
-  render() {
     console.log("App render")
-    console.log(this.props)
+    console.log(state)
+    console.log(location)
 
-      return <React.Fragment>
-        <CssBaseline />
-        <Router>
-          <Switch>
-            <Route exact path="/"><Redirect to="/account" /></Route>
-            <Route path="/account/:accountId/settings" component={AccountSettings} />
-            <Route path="/account/:accountId/addContact/:contactId" component={JamiMessenger} />
-            <Route path="/account/:accountId/conversation/:conversationId" component={JamiMessenger} />
-            <Route path="/account/:accountId" component={JamiMessenger} />
-            <Route path="/account" component={AccountSelection} />
-            <Route component={NotFoundPage} />
-          </Switch>
-        </Router>
-        {!this.state.authenticated && <SignInPage open={!this.state.authenticated}/>}
-      </React.Fragment>
-  }
+    if (!state.loaded) {
+      return <Container><CircularProgress /></Container>
+    } else if (!state.auth.setupComplete) {
+      return <Switch>
+          <Route path="/setup" component={ServerSetup} />
+          <Route><Redirect to="/setup" /></Route>
+        </Switch>
+    }
+    return <React.Fragment>
+      <CssBaseline />
+        <Switch>
+          <Route exact path="/"><Redirect to="/account" /></Route>
+          <Route path="/account/:accountId/settings" component={AccountSettings} />
+          <Route path="/account/:accountId/addContact/:contactId" component={JamiMessenger} />
+          <Route path="/account/:accountId/conversation/:conversationId" component={JamiMessenger} />
+          <Route path="/account/:accountId" component={JamiMessenger} />
+          <Route path="/account" component={AccountSelection} />
+          <Route component={NotFoundPage} />
+        </Switch>
+      {!state.auth.authenticated && <SignInPage open={!state.auth.authenticated}/>}
+    </React.Fragment>
 }
 
-export default App
\ No newline at end of file
+export default App
diff --git a/client/src/AuthManager.js b/client/src/AuthManager.js
index 09aef64..4f504db 100644
--- a/client/src/AuthManager.js
+++ b/client/src/AuthManager.js
@@ -17,39 +17,114 @@
  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
-//import cookie from 'cookie';
-
 class AuthManager {
     constructor() {
         console.log("AuthManager()")
-        this.authenticated = true//'connect.sid' in cookie.parse(document.cookie)
         this.authenticating = false
+
+        this.state = {
+            initialized: false,
+            authenticated: true,
+            setupComplete: true,
+            error: false
+        }
+
         this.tasks = []
         this.onAuthChanged = undefined
     }
-    setOnAuthChanged(onAuthChanged) {
-        this.onAuthChanged = onAuthChanged
-    }
 
     isAuthenticated() {
-        return this.authenticated
+        return this.state.authenticated
     }
 
-    authenticate() {
+    getState() {
+        return this.state
+    }
+
+    init(cb) {
+        this.onAuthChanged = cb
+        if (this.state.initialized || this.authenticating)
+            return
+        console.log("Init")
+        this.authenticating = true
+        fetch('/auth')
+            .then(async (response) => {
+                this.authenticating = false
+                this.state.initialized = true
+                console.log("Init ended")
+                console.log(response)
+                if (response.status === 200) {
+                    const jsonData = await response.json()
+                    Object.assign(this.state, {
+                        authenticated: true,
+                        setupComplete: true,
+                        error: false,
+                        user: { username: jsonData.username, type: jsonData.type }
+                    })
+                } else if (response.status === 401) {
+                    const jsonData = await response.json()
+                    Object.assign(this.state, {
+                        authenticated: false,
+                        setupComplete: 'setupComplete' in jsonData ? jsonData.setupComplete : true,
+                        error: false
+                    })
+                } else {
+                    this.state.error = true
+                }
+                console.log("New auth state")
+                console.log(this.state)
+
+                if (this.onAuthChanged)
+                    this.onAuthChanged(this.state)
+            })
+    }
+
+    deinit() {
+        console.log("Deinit")
+        this.onAuthChanged = undefined
+    }
+
+    async setup(password) {
+        if (this.authenticating || this.state.setupComplete)
+            return
+        console.log("Starting setup")
+        this.authenticating = true
+        const response = await fetch(`/setup`, {
+            method: 'POST',
+            headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({ password })
+        })
+        console.log(response)
+        if (response.ok) {
+            console.log("Success, going home")
+            //history.replace('/')
+        } else {
+        }
+        this.authenticating = false
+        this.state.setupComplete = true
+        if (this.onAuthChanged)
+            this.onAuthChanged(this.state)
+        return response.ok
+    }
+
+    authenticate(username, password) {
         if (this.authenticating)
             return
         console.log("Starting authentication")
         this.authenticating = true
-        fetch('/api/localLogin?username=local&password=local', { method:"POST" })
+        fetch(`/auth/local?username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`, { method:"POST" })
             .then(response => {
                 console.log(response)
                 this.authenticating = false
-                this.authenticated = response.ok && response.status === 200
+                this.state.authenticated = response.ok && response.status === 200
                 if (this.onAuthChanged)
-                    this.onAuthChanged(this.authenticated)
+                    this.onAuthChanged(this.state)
                 while (this.tasks.length !== 0) {
                     const task = this.tasks.shift()
-                    if (this.authenticated)
+                    if (this.state.authenticated)
                         fetch(task.url, task.init).then(res => task.resolve(res))
                     else
                         task.reject(new Error("Authentication failed"))
@@ -59,14 +134,14 @@
 
     disconnect() {
         console.log("Disconnect")
-        this.authenticated = false
+        this.state.authenticated = false
         if (this.onAuthChanged)
-            this.onAuthChanged(this.authenticated)
+            this.onAuthChanged(this.state)
     }
 
     fetch(url, init) {
         console.log(`get ${url}`)
-        if (!this.authenticated) {
+        if (!this.state.authenticated) {
             return new Promise((resolve, reject) => this.tasks.push({url, init, resolve, reject}))
         }
         return fetch(url, init)
diff --git a/client/src/components/AccountList.js b/client/src/components/AccountList.js
index 6f38c3c..38f1291 100644
--- a/client/src/components/AccountList.js
+++ b/client/src/components/AccountList.js
@@ -1,29 +1,19 @@
-import React from 'react';
+import React from 'react'
+import { Avatar, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'
+import { PersonRounded } from '@material-ui/icons';
 
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemText from '@material-ui/core/ListItemText';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import Avatar from '@material-ui/core/Avatar';
-import PersonRoundedIcon from '@material-ui/icons/PersonRounded';
-
-class AccountList extends React.Component {
-  render() {
-    return (
-        <List>
-          {
-            this.props.accounts.map(account => <ListItem button key={account.getId()} onClick={() => this.props.onClick(account)}>
-              <ListItemAvatar>
-                <Avatar>
-                  <PersonRoundedIcon />
-                </Avatar>
-              </ListItemAvatar>
-              <ListItemText primary={account.getDisplayName()} secondary={account.getDisplayUri()} />
-            </ListItem>
-            )
-          }
-        </List>)
-  }
+export default function AccountList(props) {
+  return <List>
+    {
+      props.accounts.map(account => {
+        const displayName = account.getDisplayNameNoFallback()
+        return <ListItem button key={account.getId()} onClick={() => props.onClick(account)}>
+          <ListItemAvatar>
+            <Avatar>{displayName ? displayName[0].toUpperCase() : <PersonRounded />}</Avatar>
+          </ListItemAvatar>
+          <ListItemText primary={account.getDisplayName()} secondary={account.getDisplayUri()} />
+        </ListItem>
+      })
+    }
+  </List>
 }
-
-export default AccountList;
\ No newline at end of file
diff --git a/client/src/components/ConversationListItem.js b/client/src/components/ConversationListItem.js
index 7e13974..a769168 100644
--- a/client/src/components/ConversationListItem.js
+++ b/client/src/components/ConversationListItem.js
@@ -2,7 +2,7 @@
 import React from 'react'
 import Conversation from '../../../model/Conversation'
 import { useHistory, useParams } from "react-router-dom"
-import PersonIcon from '@material-ui/icons/PersonRounded'
+import { PersonRounded } from '@material-ui/icons'
 
 export default function ConversationListItem(props) {
     const { conversationId, contactId } = useParams()
@@ -22,7 +22,7 @@
                 style={{overflow:'hidden'}}
                 onClick={() => history.push(`/account/${conversation.getAccountId()}/${uri}`)}>
                 <ListItemAvatar>
-                    <Avatar>{displayName ? displayName[0].toUpperCase() : <PersonIcon />}</Avatar>
+                    <Avatar>{displayName ? displayName[0].toUpperCase() : <PersonRounded />}</Avatar>
                 </ListItemAvatar>
                 <ListItemText
                     style={{overflow:'hidden', textOverflow:'ellipsis'}}
diff --git a/client/src/components/Header.js b/client/src/components/Header.js
index d245976..c0408a1 100644
--- a/client/src/components/Header.js
+++ b/client/src/components/Header.js
@@ -32,7 +32,7 @@
             <Menu
                 id="simple-menu"
                 anchorEl={anchorEl}
-                keepMounted
+
                 open={Boolean(anchorEl)}
                 onClose={handleClose}
             >
diff --git a/client/src/components/MessageList.js b/client/src/components/MessageList.js
index d856c17..4841ea1 100644
--- a/client/src/components/MessageList.js
+++ b/client/src/components/MessageList.js
@@ -1,7 +1,7 @@
-import { Avatar, Box, Divider, Typography } from '@material-ui/core'
-import React from 'react'
 import Message from './Message'
-import PersonIcon from '@material-ui/icons/PersonRounded'
+import React from 'react'
+import { Avatar, Box, Divider, Typography } from '@material-ui/core'
+import { PersonRounded } from '@material-ui/icons'
 
 export default function MessageList(props) {
     const displayName = props.conversation.getDisplayName()
@@ -10,7 +10,7 @@
         <div className="message-list">
             <Box>
                 <Box style={{ display: 'inline-block', margin: 16, verticalAlign: 'middle' }}>
-                    <Avatar>{displayName ? displayName[0].toUpperCase() : <PersonIcon />}</Avatar>
+                    <Avatar>{displayName ? displayName[0].toUpperCase() : <PersonRounded />}</Avatar>
                 </Box>
                 <Box style={{ display: 'inline-block', verticalAlign: 'middle' }}>
                     <Typography variant="h5">{displayName}</Typography>
diff --git a/client/src/components/NewContactForm.js b/client/src/components/NewContactForm.js
index c98c9b4..5636be2 100644
--- a/client/src/components/NewContactForm.js
+++ b/client/src/components/NewContactForm.js
@@ -1,7 +1,6 @@
 import React from 'react'
-import SearchIcon from '@material-ui/icons/Search';
-import InputBase from '@material-ui/core/InputBase';
-import InputAdornment from '@material-ui/core/InputAdornment';
+import { InputBase, InputAdornment } from '@material-ui/core';
+import { SearchRounded } from '@material-ui/icons';
 
 class NewContactForm extends React.Component {
     constructor(props) {
@@ -40,11 +39,8 @@
                     type="search"
                     placeholder="Ajouter un contact"
                     onChange={this.handleChange}
-                    startAdornment={
-                        <InputAdornment position="start">
-                            <SearchIcon />
-                        </InputAdornment>
-                        } />
+                    startAdornment={<InputAdornment position="start"><SearchRounded /></InputAdornment>}
+                />
             </form>
         )
     }
diff --git a/client/src/components/SendMessageForm.js b/client/src/components/SendMessageForm.js
index 6269e3b..48770ba 100644
--- a/client/src/components/SendMessageForm.js
+++ b/client/src/components/SendMessageForm.js
@@ -1,8 +1,7 @@
 import React from 'react'
 import { makeStyles } from '@material-ui/core/styles'
 import { IconButton, InputBase, Paper, Popper } from '@material-ui/core'
-import SendIcon from '@material-ui/icons/Send'
-import EmojiIcon from '@material-ui/icons/EmojiEmotionsRounded'
+import { Send, EmojiEmotionsRounded } from '@material-ui/icons'
 import EmojiPicker from 'emoji-picker-react'
 
 const useStyles = makeStyles((theme) => ({
@@ -58,7 +57,7 @@
         className="send-message-card"
         className={classes.root}>
         <IconButton aria-describedby={id} variant="contained" color="primary" onClick={handleOpenEmojiPicker}>
-          <EmojiIcon />
+          <EmojiEmotionsRounded />
         </IconButton>
         <Popper
           id={id}
@@ -66,7 +65,7 @@
           anchorEl={anchorEl}
           onClose={handleClose}
         >
-          <EmojiPicker
+          <EmojiPicker.default
             onEmojiClick={onEmojiClick}
             disableAutoFocus={true}
             disableSkinTonePicker={true}
@@ -82,7 +81,7 @@
           onChange={handleInputChange}
         />
         <IconButton type="submit" className={classes.iconButton} aria-label="search">
-          <SendIcon />
+          <Send />
         </IconButton>
       </Paper>
     </div>
diff --git a/client/src/index.js b/client/src/index.js
index f4c1f23..054afae 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,24 +1,30 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import './index.scss';
-import App from './App';
-//import * as serviceWorker from './serviceWorker';
-const rootEl = document.getElementById('root');
+'use strict'
+import React from 'react'
+import ReactDOM from 'react-dom'
+import { BrowserRouter as Router } from 'react-router-dom'
+import App from './App.js'
+import './index.scss'
+
+//import * as serviceWorker from './serviceWorker'
+const rootEl = document.getElementById('root')
 
 const render = Component =>
 ReactDOM.render(
   <React.StrictMode>
+    <Router>
     <Component />
+    </Router>
   </React.StrictMode>,
   rootEl
-);
+)
 
 // If you want your app to work offline and load faster, you can change
 // unregister() to register() below. Note this comes with some pitfalls.
 // Learn more about service workers: https://bit.ly/CRA-PWA
-//serviceWorker.unregister();
+//serviceWorker.unregister()
 render(App)
-if (module.hot) module.hot.accept('./App', () => {
+
+if (import.meta.webpackHot) import.meta.webpackHot.accept('./App', () => {
   try {
     render(App)
   } catch (e) {
diff --git a/client/src/pages/loginDialog.jsx b/client/src/pages/loginDialog.jsx
index 93f9f59..4e15ae1 100644
--- a/client/src/pages/loginDialog.jsx
+++ b/client/src/pages/loginDialog.jsx
@@ -105,7 +105,7 @@
             submitted: true,
             loading: true
         })
-        authManager.authenticate()
+        authManager.authenticate('admin', 'admin')
         /*fetch('/api/localLogin?username=none&password=none', {
             header: { "Content-Type": "application/json" },
             method: "POST",
diff --git a/client/src/pages/serverSetup.jsx b/client/src/pages/serverSetup.jsx
new file mode 100644
index 0000000..70f9342
--- /dev/null
+++ b/client/src/pages/serverSetup.jsx
@@ -0,0 +1,85 @@
+import React, { useState } from 'react';
+import { useHistory } from "react-router-dom";
+
+import { Box, Container, Fab, Card, CardContent, Typography, Input } from '@material-ui/core';
+import GroupAddRounded from '@material-ui/icons/GroupAddRounded';
+import { makeStyles } from '@material-ui/core/styles';
+import authManager from '../AuthManager'
+
+const useStyles = makeStyles((theme) => ({
+  root: {
+    '& > *': {
+      margin: theme.spacing(1),
+    },
+  },
+  extendedIcon: {
+    marginRight: theme.spacing(1),
+  },
+  wizardCard: {
+    borderRadius: 8,
+    maxWidth: 360,
+    margin: "16px auto"
+  }, textField: {
+    margin: theme.spacing(1),
+  }
+}))
+
+export default function ServerSetup(props) {
+  const classes = useStyles()
+  const history = useHistory();
+  const [password, setPassword] = useState('');
+  const [passwordRepeat, setPasswordRepeat] = useState('');
+
+  const isValid = () => password && password === passwordRepeat
+
+  const handleSubmit = async e => {
+    e.preventDefault()
+    if (!isValid())
+      return
+    if (await authManager.setup(password)) {
+      history.replace('/')
+    }
+  }
+
+  return (
+    <Container className='message-list'>
+      <Card  className={classes.wizardCard}>
+        <CardContent component="form" onSubmit={handleSubmit}>
+          <Typography gutterBottom variant="h5" component="h2">
+          Jami Web Node setup
+          </Typography>
+          <Typography variant="body2" color="textSecondary" component="p">
+            Welcome to the Jami web node setup.<br/>
+            Let's start by creating a new administrator account to control access to the server configuration.
+          </Typography>
+
+          <Typography variant='body1'></Typography>
+          <div><Input className={classes.textField} value="admin" name="username" autoComplete="username" disabled /></div>
+          <div><Input
+            className={classes.textField}
+            value={password}
+            onChange={e => setPassword(e.target.value)}
+            name="password"
+            type='password'
+            placeholder="New password"
+            autoComplete="new-password" />
+          </div>
+          <div><Input
+            className={classes.textField}
+            value={passwordRepeat}
+            onChange={e => setPasswordRepeat(e.target.value)}
+            name="password"
+            error={!!passwordRepeat && !isValid()}
+            type='password'
+            placeholder="Repeat password"
+            autoComplete="new-password" /></div>
+          <Box style={{ textAlign: 'center', marginTop: 16 }}>
+            <Fab variant='extended' color='primary' type='submit' disabled={!isValid()}>
+              <GroupAddRounded className={classes.extendedIcon} />
+              Create admin account
+            </Fab>
+          </Box>
+        </CardContent>
+      </Card>
+    </Container>)
+}
diff --git a/client/webpack.config.js b/client/webpack.config.js
index f3b38cd..82caf43 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -1,27 +1,38 @@
 'use strict'
-const path = require('path')
-require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') })
-const HtmlWebpackPlugin = require('html-webpack-plugin')
+
+import dotenv from 'dotenv'
+dotenv.config({ path: resolve(import.meta.url, '..', '.env') })
+
+import { resolve } from 'path'
+import HtmlWebpackPlugin from 'html-webpack-plugin'
+
+import { fileURLToPath } from 'url';
+import { dirname } from 'path';
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
 const mode = process.env.NODE_ENV || 'development'
 
-let entry = [path.resolve(__dirname, 'src', 'index.js')]
+let entry = [resolve(__dirname, 'src', 'index.js')]
 let plugins = [new HtmlWebpackPlugin({
-    template: path.resolve(__dirname, 'src', 'index.ejs')
+    template: resolve(__dirname, 'src', 'index.ejs')
 })]
 let devtool = undefined
+let babelLoaderPlugins = ["@babel/plugin-transform-runtime"]
 
 if (mode === 'development') {
-    const webpack = require('webpack')
-    entry = ['react-hot-loader/patch', 'webpack-hot-middleware/client', ...entry]
-    plugins = [new webpack.HotModuleReplacementPlugin(), ...plugins]
+    const webpack = (await import('webpack')).default
+    const ReactRefreshWebpackPlugin = (await import('@pmmmwh/react-refresh-webpack-plugin')).default
+    entry = ['webpack-hot-middleware/client', ...entry]
+    plugins = [new webpack.HotModuleReplacementPlugin(), new ReactRefreshWebpackPlugin(), ...plugins]
+    babelLoaderPlugins = [...babelLoaderPlugins, "react-refresh/babel"]
     devtool = 'inline-source-map'
 }
 console.log(`Webpack configured for ${mode}`)
 
-module.exports = {
+export default {
     entry,
     output: {
-        path: path.resolve(__dirname, 'dist'),
+        path: resolve(__dirname, 'dist'),
         filename: 'bundle.js',
         publicPath: '/'
     },
@@ -31,14 +42,20 @@
         rules: [
             {
                 test: /\.jsx?/,
+                resolve: {
+                    fullySpecified: false
+                },
                 exclude: /node_modules/,
                 use: {
                     loader: 'babel-loader',
                     options: {
-                        presets: [['@babel/preset-env', {
-                            useBuiltIns: 'entry',
-                            corejs:{ version: "3.10", proposals: true },
-                        }], '@babel/preset-react']
+                        plugins: babelLoaderPlugins,
+                        presets: [
+                            ['@babel/preset-env', {
+                                useBuiltIns: 'entry',
+                                corejs:{ version: "3.10", proposals: true },
+                            }],
+                            '@babel/preset-react']
                     }
                 }
             },