use ESM, add server setup, cleanup
Change-Id: Iafac35c2082523ae98c31017d9bad5c4d6e18ef3
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>)
+}