Skip to content

Commit c1c230f

Browse files
committed
Move all the auth code and login dialog into a single component.
1 parent 4d2766b commit c1c230f

File tree

4 files changed

+133
-120
lines changed

4 files changed

+133
-120
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ Don't expect the code to do anything particularly useful!
1515

1616
## TODO
1717

18-
* Move authentication code out of App.jsx.
18+
Huh. I did everything I was planning. And just in time for EDB's Wellness Friday!

frontend/src/components/App.jsx

+7-96
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useEffect, useState} from "react";
1+
import React, {useState} from "react";
22
import {render} from 'react-dom';
33
import ButtonAppBar from './AppBar';
44
import LeadList from './LeadList';
@@ -7,11 +7,9 @@ import {createTheme, ThemeProvider} from '@mui/material/styles';
77
import Alert from '@mui/material/Alert';
88
import Box from '@mui/material/Box';
99
import Grid from '@mui/material/Grid';
10-
import Cookies from "universal-cookie";
11-
import {stringAvatar} from "../utils";
10+
import {anonUser} from "./Authentication";
1211
import {AppContext} from "./AppContext";
1312

14-
const cookies = new Cookies();
1513

1614
const darkTheme = createTheme({
1715
palette: {
@@ -36,111 +34,23 @@ defaultDark = JSON.parse(localStorage.getItem('darkMode')) ?? osDark;
3634

3735

3836
export function App() {
39-
const anonUser = {
40-
username: '',
41-
fullname: 'Anonymous User',
42-
avatarProps: stringAvatar('Anonymous User'),
43-
email: '',
44-
password: '',
45-
isAuthenticated: false
46-
};
47-
4837
const [user, setUser] = useState(anonUser);
4938
const [darkMode, setDarkMode] = useState(defaultDark);
5039
const [error, setError] = useState("");
5140

52-
// Retrieve any session details, or clear them if necessary
53-
const getSession = () => {
54-
fetch("/api/session/", {
55-
credentials: "same-origin",
56-
})
57-
.then((res) => res.json())
58-
.then((data) => {
59-
console.log(data);
60-
if (data.isAuthenticated) {
61-
setUser({
62-
username: data.username,
63-
fullname: data.fullname,
64-
avatarProps: stringAvatar(data.fullname),
65-
email: data.email,
66-
password: '',
67-
isAuthenticated: true
68-
});
69-
setError('');
70-
} else {
71-
setUser(anonUser);
72-
setError('');
73-
}
74-
})
75-
.catch((err) => {
76-
console.log(err);
77-
});
78-
}
79-
80-
// Did we get a good response from our request?
81-
const isResponseOk = (response) => {
82-
if (response.status >= 200 && response.status <= 299) {
83-
return response.json();
84-
} else {
85-
throw Error(response.statusText);
86-
}
87-
}
88-
89-
// Clear any previous errors
90-
const clearError = () => {
91-
setError('');
92-
}
93-
94-
// Attempt to login
95-
const login = (username, password, cbSuccess) => {
96-
fetch("/api/login/", {
97-
method: "POST", headers: {
98-
"Content-Type": "application/json", "X-CSRFToken": cookies.get("csrftoken"),
99-
}, credentials: "same-origin", body: JSON.stringify({username: username, password: password}),
100-
})
101-
.then(isResponseOk)
102-
.then(() => {
103-
getSession();
104-
cbSuccess();
105-
})
106-
.catch((err) => {
107-
console.log(err);
108-
setError('Incorrect username or password.');
109-
});
110-
}
111-
112-
// Clear the session out
113-
const logout = () => {
114-
fetch("/api/logout", {
115-
credentials: "same-origin",
116-
})
117-
.then(isResponseOk)
118-
.then((data) => {
119-
console.log(data);
120-
setUser(anonUser);
121-
setError('');
122-
})
123-
.catch((err) => {
124-
console.log(err);
125-
});
126-
};
12741

12842
// Toggle the theme
12943
const changeTheme = () => {
13044
localStorage.setItem('darkMode', JSON.stringify(!darkMode));
13145
setDarkMode(!darkMode);
13246
}
13347

134-
// See if we can get session data
135-
useEffect(() => {
136-
getSession()
137-
}, []);
13848

139-
return (<AppContext.Provider value={{user, darkMode, error}}>
49+
return (
50+
<AppContext.Provider value={{user, darkMode, error, setUser, setError}}>
14051
<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
14152
<CssBaseline/>
142-
<ButtonAppBar appState={user} handleLogin={login} handleLogout={logout} clearError={clearError}
143-
onChangeTheme={changeTheme}/>
53+
<ButtonAppBar onChangeTheme={changeTheme}/>
14454
<Box align="center" sx={{flexGrow: 1}} style={{marginLeft: 20, marginRight: 20}}>
14555
<Grid container align="left" maxWidth="xl" spacing={1} wrap="wrap">
14656
{!user.isAuthenticated ? <Grid item xs={12}>
@@ -157,7 +67,8 @@ export function App() {
15767
</Grid>
15868
</Box>
15969
</ThemeProvider>
160-
</AppContext.Provider>)
70+
</AppContext.Provider>
71+
)
16172
}
16273

16374
const container = document.getElementById("app");

frontend/src/components/AppBar.jsx

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import {useContext, useState} from 'react';
2+
import {useContext, useRef, useState} from 'react';
33
import AppBar from '@mui/material/AppBar';
44
import Box from '@mui/material/Box';
55
import Toolbar from '@mui/material/Toolbar';
@@ -15,23 +15,26 @@ import MenuItem from '@mui/material/MenuItem';
1515
import StorageTwoToneIcon from '@mui/icons-material/StorageTwoTone';
1616
import Brightness4Icon from '@mui/icons-material/Brightness4';
1717
import Brightness7Icon from '@mui/icons-material/Brightness7';
18-
import LoginDialog from './Authentication';
18+
import {LoginDialog} from './Authentication';
1919
import {AppContext} from "./AppContext";
2020

21-
function ResponsiveAppBar({handleLogin, handleLogout, clearError, onChangeTheme}) {
21+
function ResponsiveAppBar({onChangeTheme}) {
2222
const [anchorElNav, setAnchorElNav] = React.useState(null);
2323
const [anchorElUser, setAnchorElUser] = React.useState(null);
2424
const [showLoginDialog, setShowLoginDialog] = useState(false);
2525

2626
const appContext = useContext(AppContext);
27+
const logoutRef = useRef();
2728

2829
const handleOpenNavMenu = (event) => {
2930
setAnchorElNav(event.currentTarget);
3031
};
32+
3133
const handleOpenUserMenu = (event) => {
3234
setAnchorElUser(event.currentTarget);
3335
};
3436

37+
3538
const handleCloseNavMenu = () => {
3639
setAnchorElNav(null);
3740
};
@@ -40,17 +43,15 @@ function ResponsiveAppBar({handleLogin, handleLogout, clearError, onChangeTheme}
4043
setAnchorElUser(null);
4144
};
4245

46+
4347
const onLogin = () => {
4448
setAnchorElUser(null);
4549
setShowLoginDialog(true);
4650
}
4751

48-
const onLogout = () => {
49-
setAnchorElUser(null);
50-
handleLogout();
51-
};
5252

5353
return (<>
54+
<LoginDialog open={showLoginDialog} setShowLoginDialog={setShowLoginDialog} ref={logoutRef} />
5455
<AppBar position="static">
5556
<Container maxWidth="xl">
5657
<Toolbar disableGutters>
@@ -167,7 +168,7 @@ function ResponsiveAppBar({handleLogin, handleLogout, clearError, onChangeTheme}
167168
<MenuItem disabled={appContext.user.isAuthenticated} aria-label='login'
168169
onClick={onLogin}>Login</MenuItem>
169170
<MenuItem disabled={!appContext.user.isAuthenticated} aria-label='logout'
170-
onClick={onLogout}>Logout</MenuItem>
171+
onClick={() => handleCloseUserMenu() & logoutRef.current.handleLogout()}>Logout</MenuItem>
171172
</Menu>
172173

173174
{/* Darkmode toggler */}
@@ -178,8 +179,6 @@ function ResponsiveAppBar({handleLogin, handleLogout, clearError, onChangeTheme}
178179
</Toolbar>
179180
</Container>
180181
</AppBar>
181-
{showLoginDialog && (<LoginDialog open={showLoginDialog} handleLogin={handleLogin} clearError={clearError}
182-
setShowLoginDialog={setShowLoginDialog} />)}
183182
</>);
184183
}
185184

frontend/src/components/Authentication.jsx

+116-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import {useContext} from 'react';
2+
import {forwardRef, useContext, useEffect, useImperativeHandle} from 'react';
33
import Button from '@mui/material/Button';
44
import TextField from '@mui/material/TextField';
55
import Dialog from '@mui/material/Dialog';
@@ -15,38 +15,141 @@ import Stack from '@mui/material/Stack';
1515
import Alert from '@mui/material/Alert';
1616
import {AppContext} from "./AppContext";
1717
import {stringAvatar} from "../utils";
18+
import Cookies from "universal-cookie";
1819

1920

20-
export default function LoginDialog(props) {
21+
export const anonUser = {
22+
username: '',
23+
fullname: 'Anonymous User',
24+
avatarProps: stringAvatar('Anonymous User'),
25+
email: '',
26+
password: '',
27+
isAuthenticated: false
28+
};
29+
30+
31+
const cookies = new Cookies();
32+
33+
34+
export const LoginDialog = forwardRef((props, ref) => {
2135
const [username, setUsername] = React.useState('');
2236
const [password, setPassword] = React.useState('');
2337

2438
const appContext = useContext(AppContext);
2539

40+
41+
// Handle show password
42+
const [showPassword, setShowPassword] = React.useState(false);
43+
const handleClickShowPassword = () => setShowPassword((show) => !show);
44+
const handleMouseDownPassword = (event) => {
45+
event.preventDefault();
46+
};
47+
48+
2649
// Close the dialog
2750
const handleClose = () => {
2851
setUsername('');
2952
setPassword('');
30-
props.clearError();
53+
appContext.setError('');
3154

3255
props.setShowLoginDialog(false);
3356
};
3457

35-
// Handle show password
36-
const [showPassword, setShowPassword] = React.useState(false);
37-
const handleClickShowPassword = () => setShowPassword((show) => !show);
38-
const handleMouseDownPassword = (event) => {
39-
event.preventDefault();
58+
59+
// Did we get a good response from our request?
60+
const isResponseOk = (response) => {
61+
if (response.status >= 200 && response.status <= 299) {
62+
return response.json();
63+
} else {
64+
throw Error(response.statusText);
65+
}
66+
};
67+
68+
69+
// Retrieve any session details, or clear them if necessary
70+
const doGetSession = () => {
71+
fetch("/api/session/", {
72+
credentials: "same-origin",
73+
})
74+
.then((res) => res.json())
75+
.then((data) => {
76+
console.log(data);
77+
if (data.isAuthenticated) {
78+
appContext.setUser({
79+
username: data.username,
80+
fullname: data.fullname,
81+
avatarProps: stringAvatar(data.fullname),
82+
email: data.email,
83+
password: '',
84+
isAuthenticated: true
85+
});
86+
appContext.setError('');
87+
} else {
88+
appContext.setUser(anonUser);
89+
appContext.setError('');
90+
}
91+
})
92+
.catch((err) => {
93+
console.log(err);
94+
});
4095
};
4196

97+
4298
// Attempt a login
43-
const login = (event) => {
99+
const handleLogin = (event) => {
44100
event.preventDefault();
45-
props.handleLogin(username, password, handleClose);
101+
doLogin(username, password, handleClose);
102+
};
103+
104+
const doLogin = (username, password, cbSuccess) => {
105+
fetch("/api/login/", {
106+
method: "POST", headers: {
107+
"Content-Type": "application/json", "X-CSRFToken": cookies.get("csrftoken"),
108+
}, credentials: "same-origin", body: JSON.stringify({username: username, password: password}),
109+
})
110+
.then(isResponseOk)
111+
.then(() => {
112+
doGetSession();
113+
cbSuccess();
114+
})
115+
.catch((err) => {
116+
console.log(err);
117+
appContext.setError('Incorrect username or password.');
118+
});
119+
};
120+
121+
122+
const doLogout = () => {
123+
fetch("/api/logout", {
124+
credentials: "same-origin",
125+
})
126+
.then(isResponseOk)
127+
.then((data) => {
128+
console.log(data);
129+
appContext.setUser(anonUser);
130+
appContext.setError('');
131+
})
132+
.catch((err) => {
133+
console.log(err);
134+
});
46135
}
47136

137+
// Allow access to doLogout from elsewhere
138+
useImperativeHandle(ref, () => ({
139+
handleLogout: () => {
140+
doLogout();
141+
}
142+
}));
143+
144+
145+
// Get the session data (if present)... but just once.
146+
useEffect(() => {
147+
doGetSession();
148+
}, []);
149+
150+
48151
return (<>
49-
<Dialog open={open}>
152+
<Dialog open={props.open}>
50153
<DialogTitle>Login</DialogTitle>
51154
<DialogContent>
52155
<Stack spacing={2}>
@@ -97,8 +200,8 @@ export default function LoginDialog(props) {
97200
</DialogContent>
98201
<DialogActions>
99202
<Button aria-label='cancel' onClick={handleClose}>Cancel</Button>
100-
<Button aria-label='login' disabled={username == '' || password == ''} onClick={login}>Login</Button>
203+
<Button aria-label='login' disabled={username == '' || password == ''} onClick={handleLogin}>Login</Button>
101204
</DialogActions>
102205
</Dialog>
103206
</>);
104-
}
207+
});

0 commit comments

Comments
 (0)