Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Commit

Permalink
user profile management
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaquiery committed Feb 27, 2023
1 parent 23b7962 commit ead98bf
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 39 deletions.
13 changes: 11 additions & 2 deletions backend/backend_django/galvanalyser/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,11 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer):
users = serializers.SerializerMethodField()

def get_users(self, instance) -> list:
return UserSerializer(instance.user_set.all(), many=True, context={'request': self.context['request']}).data
return UserSerializer(
instance.user_set.filter(is_active=True),
many=True,
context={'request': self.context['request']}
).data

class Meta:
model = Group
Expand All @@ -343,7 +347,11 @@ def get_description(self, instance):
return self.context.get('description')

def get_users(self, instance) -> list:
return UserSerializer(instance.user_set.all(), many=True, context={'request': self.context['request']}).data
return UserSerializer(
instance.user_set.filter(is_active=True),
many=True,
context={'request': self.context['request']}
).data

def get_name(self, instance):
return self.context.get('name', instance.name)
Expand Down Expand Up @@ -375,6 +383,7 @@ class Meta:
'url',
'id',
'username',
'email',
'first_name',
'last_name',
'is_active',
Expand Down
32 changes: 32 additions & 0 deletions backend/backend_django/galvanalyser/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def post(self, request, fmt=None):
return super(LoginView, self).post(request=request, format=fmt)
return Response({'detail': "Anonymous login not allowed"}, status=401)

def get_post_response_data(self, request, token, instance):
return {
**UserSerializer(request.user, context={'request': request}).data,
'token': token
}

@extend_schema(
description="Log out current Knox Token.",
Expand Down Expand Up @@ -779,6 +784,33 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.filter(is_active=True)

@action(detail=True, methods=['PATCH'])
def update_profile(self, request, pk: int = None):
try:
user = User.objects.get(id=pk)
except User.DoesNotExist:
return error_response("User not found", 404)
if user != request.user:
return error_response("You may only edit your own details", 401)
email = request.data.get('email')
if email:
try:
validators.validate_email(email)
except validators.ValidationError:
return error_response("Invalid email")
password = request.data.get('password')
if password and not len(password) > 7:
return error_response("Password must be at least 7 characters")
current_password = request.data.get('currentPassword')
if not user.check_password(current_password):
return error_response("You must include the correct current password", 401)
if email:
user.email = email
if password:
user.set_password(password)
user.save()
return Response(UserSerializer(user, context={'request': request}).data)


class GroupViewSet(viewsets.ReadOnlyModelViewSet):
"""
Expand Down
53 changes: 40 additions & 13 deletions frontend/src/APIConnection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {UpdateResult} from "./UserProfile";

export type User = {
url: string,
id: number,
username: string,
first_name: string,
last_name: string,
is_staff: boolean,
is_superuser: boolean,
token: string,
url: string;
id: number;
username: string;
email: string;
first_name: string;
last_name: string;
is_staff: boolean;
is_superuser: boolean;
token: string;
}

export interface APIObject {
Expand Down Expand Up @@ -125,17 +128,41 @@ export class APIConnection {
})
}

update_user(email: string, password: string, currentPassword: string): Promise<UpdateResult> {
if (!this.user)
return new Promise(() => {})
return fetch(
`${this.user.url}update_profile/`,
{
method: 'PATCH',
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${this.user.token}`
},
body: JSON.stringify({email, password, currentPassword})
}
)
.then(r => {
if (r.status === 200)
return r.json()
.then(user => {
this.user = {...user, token: this.user?.token}
})
.then(() => ({success: true, message: 'Updated successfully'}))
return r.json()
.then(r => ({success: false, message: r.error}))
})
}

login(username: string, password: string) {
let headers = new Headers();
headers.set('Authorization', 'Basic ' + btoa(username + ":" + password));
headers.set('Accept', 'application/json');
return fetch(this.url + 'login/', {method: 'POST', headers: headers})
.then(r => r.json())
.then(r => {
this.user = {
...r.user,
token: r.token
}
.then(user => {
this.user = user
window.localStorage.setItem('user', JSON.stringify(this.user))
console.info(`Logged in as ${this.user?.username}`)
return true
Expand Down
24 changes: 14 additions & 10 deletions frontend/src/Core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ import GroupAddIcon from '@mui/icons-material/GroupAdd';
import Equipment from "./Equipment"
import Datasets from "./Datasets"
import TableChartIcon from '@mui/icons-material/TableChart';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import BatteryUnknownIcon from '@mui/icons-material/BatteryUnknown';
import BackupIcon from '@mui/icons-material/Backup';
import { makeStyles} from "@mui/styles";
Expand All @@ -47,6 +42,7 @@ import { ReactComponent as GalvanalyserLogo } from './Galvanalyser-logo.svg';
import Connection from "./APIConnection";
import Stack from "@mui/material/Stack";
import Tokens from "./Tokens";
import UserProfile from "./UserProfile";

const PrivateRoute = (component: JSX.Element) => {
const logged = Connection.is_logged_in;
Expand Down Expand Up @@ -153,8 +149,10 @@ export default function Core() {
const isCellsPath = matchPath({path: cellsPath, end: true}, pathname) !== null
const equipmentPath = "/equipment"
const isEquipmentPath = matchPath({path: equipmentPath, end: true}, pathname) !== null
const userPath = "/users"
const isUsersPath = matchPath({path: userPath, end: true}, pathname) !== null
const usersPath = "/users"
const isUsersPath = matchPath({path: usersPath, end: true}, pathname) !== null
const profilePath = "/profile"
const isProfilePath = matchPath({path: profilePath, end: true}, pathname) !== null
const tokenPath = "/tokens"
const isTokenPath = matchPath({path: tokenPath, end: true}, pathname) !== null

Expand Down Expand Up @@ -203,7 +201,7 @@ export default function Core() {
<Divider/>
<ListItem button
selected={isUsersPath}
component={Link} to={userPath}>
component={Link} to={usersPath}>
<ListItemIcon>
<GroupAddIcon/>
</ListItemIcon>
Expand Down Expand Up @@ -236,7 +234,12 @@ export default function Core() {
User: {userDisplayName}
</Button>
<Button color="inherit" onClick={() => {
navigate('/tokens')
navigate(profilePath)
}}>
Manage Profile
</Button>
<Button color="inherit" onClick={() => {
navigate(tokenPath)
}}>
Manage API Tokens
</Button>
Expand Down Expand Up @@ -277,7 +280,8 @@ export default function Core() {
<Route path={cellsPath} element={Cells()} />
<Route path={equipmentPath} element={Equipment()} />
<Route path={harvestersPath} element={Harvesters()} />
<Route path={userPath} element={ActivateUsers()} />
<Route path={usersPath} element={ActivateUsers()} />
<Route path={profilePath} element={UserProfile()} />
<Route path={tokenPath} element={Tokens()} />
<Route index element={Datasets()} />
</Route>
Expand Down
31 changes: 17 additions & 14 deletions frontend/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Connection, {User} from "./APIConnection";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";

const useStyles = makeStyles((theme) => ({
paper: {
Expand Down Expand Up @@ -184,20 +185,22 @@ export default function Login() {
id="password"
autoComplete="current-password"
/>
{registerMode && <TextField
variant="outlined"
margin="normal"
required
fullWidth
name="email"
label="Email address"
type="email"
onChange={handleEmailChange}
onKeyDown={handleEnterKey}
ref={email_input}
id="email"
autoComplete="email"
/>}
{registerMode && <Tooltip title="An email address is required in case you forget your password.">
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="email"
label="Email address"
type="email"
onChange={handleEmailChange}
onKeyDown={handleEnterKey}
ref={email_input}
id="email"
autoComplete="email"
/>
</Tooltip>}
{error &&
<Alert severity="error">
{error}
Expand Down
101 changes: 101 additions & 0 deletions frontend/src/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, {SyntheticEvent, useState} from 'react';
import TextField from '@mui/material/TextField';
import Paper from '@mui/material/Paper';
import Container from '@mui/material/Container';
import Connection from "./APIConnection";
import useStyles from "./UseStyles";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
import Alert from "@mui/material/Alert";
import Snackbar from "@mui/material/Snackbar";

export type UpdateResult = {
success: boolean;
message: string;
}

export default function UserProfile() {
const classes = useStyles();
const [email, setEmail] = useState<string>(Connection.user?.email || '')
const [password, setPassword] = useState<string>('')
const [currentPassword, setCurrentPassword] = useState<string>('')
const [updateResult, setUpdateResult] = useState<UpdateResult|null>()
const [open, setOpen] = useState<boolean>(false)

const updateUser = () => Connection.update_user(email, password, currentPassword)
.then(setUpdateResult)
.then(() => {
setOpen(true)
setEmail(Connection.user?.email || '')
setPassword('')
setCurrentPassword('')
})

const handleClose = (e: any, reason?: string) => {
if (reason !== 'clickaway')
setOpen(false)
}

return (
<Container maxWidth="lg" className={classes.container} component="form">
<Paper sx={{padding: 2}}>
<Stack spacing={2}>
<Typography variant="h5">{Connection.user?.username} profile</Typography>
<TextField
name="email"
label="Email"
value={email}
InputProps={{
classes: {
input: classes.resize,
},
}}
onChange={e => setEmail(e.target.value)}
/>
<TextField
name="password"
label="Update password"
type="password"
helperText="Must be at least 8 characters"
value={password}
InputProps={{
classes: {
input: classes.resize,
},
}}
onChange={e => setPassword(e.target.value)}
error={password !== undefined && password.length > 0 && password.length < 8}
/>
<TextField
name="currentPassword"
label="Current password"
type="password"
required
value={currentPassword}
InputProps={{
classes: {
input: classes.resize,
},
}}
onChange={e => setCurrentPassword(e.target.value)}
/>
<Button
role="submit"
fullWidth
variant="contained"
color="primary"
onClick={updateUser}
>
Update profile
</Button>
</Stack>
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity={updateResult?.success? 'success' : 'error'} sx={{ width: '100%' }}>
{updateResult?.message}
</Alert>
</Snackbar>
</Paper>
</Container>
);
}

0 comments on commit ead98bf

Please sign in to comment.