Skip to content

Commit

Permalink
Merge pull request #176 from cornell-dti/add-admin
Browse files Browse the repository at this point in the history
Add admin
  • Loading branch information
mluo24 authored Aug 6, 2023
2 parents bb70018 + ea57c36 commit 04390cb
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 80 deletions.
9 changes: 1 addition & 8 deletions backend/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
checkIsAuthorized,
checkIsAuthorizedFromToken,
logReqBody,
getAllAllowedUsers,
} from './middleware/auth-middleware'
import { unless } from './middleware/unless'
import { db } from './config'
Expand Down Expand Up @@ -99,14 +100,6 @@ export const removeAllowedUser = async (email: string) => {
})
}

/** Get all administrative users */
export const getAllAllowedUsers = async () => {
const adminCollection = await adminRef.get()
return adminCollection.docs.map((adminDoc) => {
return adminDoc.data()
})
}

app.post('/admin', (req, res) => {
const { name, email } = req.body
addAllowedUser(name, email)
Expand Down
28 changes: 14 additions & 14 deletions backend/functions/src/middleware/auth-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import QueryDocumentSnapshot = firestore.QueryDocumentSnapshot

const { db } = require('../config')
import * as admin from 'firebase-admin'
import { Request, Response, NextFunction } from 'express'
import { logger } from 'firebase-functions'
import { firestore } from 'firebase-admin'
import QuerySnapshot = firestore.QuerySnapshot
import DocumentData = firestore.DocumentData

// get data in global scope so this is shared across all function invocations:
// retrieves the allowed users in the database
function getAllowedUsers() {
const users: string[] = []
db.collection('allowed_users')
.get()
.then((querySnapshot: QuerySnapshot) => {
querySnapshot.forEach((doc) => {
users.push(doc.id)
})
})
return users
export async function getAllAllowedUsers() {
const adminCollection = await db.collection('allowed_users').get()
return adminCollection.docs.map(
(adminDoc: QueryDocumentSnapshot<DocumentData>) => {
return adminDoc.data()
}
)
}

const allowedUsers = getAllowedUsers()

// function courtesy of https://itnext.io/how-to-use-firebase-auth-with-a-custom-node-backend-99a106376c8a
// checks if valid based on the auth token in firebase admin
export function checkAuth(req: Request, res: Response, next: NextFunction) {
Expand Down Expand Up @@ -64,7 +61,10 @@ export async function checkIsAuthorizedFromToken(
logger.info(
`${req.method} request on endpoint ${req.originalUrl} called by user ${user.displayName} with uid ${user.uid}`
)
return !!(user.email && allowedUsers.includes(user.email))
const allowedUsersEmails = (await getAllAllowedUsers()).map(
(user: DocumentData) => user.email
)
return user.email && allowedUsersEmails.includes(user.email)
}

export function checkIsAuthorized(
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,15 @@ const App = () => {
.catch((err) => console.log(err))
}

// TODO: allow to edit admin information
const addAdmin = () => {}
const addAdmin = (admin: Admin) => {
axios
.post(`${API_ROOT}/admin`, admin)
.then(() => {
administrators.push(admin)
loadAdministrators()
})
.catch((err) => console.log(err))
}

const [semesterAdded, setSemesterAdded] = useState<boolean>(false)
const addSemester = (selectedSeason: string, year: string) => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/context/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface SettingsContextType {
changeCurrRoster: (event: SelectChangeEvent) => Promise<void>
changeSurveyAvailability: () => Promise<void>
removeAdmin: (admin: Admin) => void
addAdmin: () => void
addAdmin: (admin: Admin) => void
addSemester: (selectedSeason: string, year: string) => void
setSemesterAdded: (value: React.SetStateAction<boolean>) => void
}
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/firebase/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ export async function signInWithMicrosoft() {
}

export async function adminSignIn() {
await signInWithMicrosoft().then((res) => {
window.localStorage.setItem('authToken', res)
})
await signInWithMicrosoft()
.then((res) => {
window.localStorage.setItem('authToken', res)
})
.catch((err) => {
console.log(err)
})
}

export function logOut() {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/modules/Core/Types/Admin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface Admin {
name: string
email: string
}
}
2 changes: 1 addition & 1 deletion frontend/src/modules/Core/Types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export * from '@core/Types/EmailTemplates'
export * from '@core/Types/Course'
export * from '@core/Types/Student'
export * from '@core/Components'
export * from '@core/Types/Admin'
export * from '@core/Types/Admin'
71 changes: 71 additions & 0 deletions frontend/src/modules/Settings/Components/AddAdminModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Typography, TextField, Button, Box } from '@mui/material'
import { AddAdminProp } from './types'
import { ZingModal } from '@core/index'
import { FormEvent, useState } from 'react'

const AddAdminModal = ({ open, handleClose, addAdmin }: AddAdminProp) => {
const [newName, setNewName] = useState('')
const [newEmail, setNewEmail] = useState('')
const [requiredField, setRequiredField] = useState(false)

return (
<ZingModal open={open} onClose={handleClose}>
<Box
component={'form'}
onSubmit={(e: FormEvent) => {
e.preventDefault()
if (newEmail.includes('@') && newEmail.includes('.')) {
addAdmin({ name: newName, email: newEmail })
handleClose()
} else {
setRequiredField(true)
}
}}
>
<ZingModal.Title onClose={handleClose}>
<Typography variant="h4" sx={{ fontWeight: '500' }}>
Add New Adminstrator
</Typography>
</ZingModal.Title>
<ZingModal.Body>
<Typography variant="h6" sx={{ fontWeight: '300', padding: 1 }}>
Name:
</Typography>
<TextField
required
onChange={(e) => setNewName(e.target.value)}
sx={{
width: '100%',
borderRadius: '15px',
}}
/>
<Typography variant="h6" sx={{ fontWeight: '300', padding: 1 }}>
Email:
</Typography>
<TextField
required
onChange={(e) => {
setNewEmail(e.target.value)
setRequiredField(false)
}}
error={requiredField}
sx={{
width: '100%',
borderRadius: '15px',
}}
/>
</ZingModal.Body>
<ZingModal.Controls>
<Button variant="outlined" color="secondary" onClick={handleClose}>
Cancel
</Button>
<Button type="submit" variant="outlined" color="primary">
Confirm
</Button>
</ZingModal.Controls>
</Box>
</ZingModal>
)
}

export default AddAdminModal
84 changes: 45 additions & 39 deletions frontend/src/modules/Settings/Components/AdministratorsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import Paper from '@mui/material/Paper'
import { DeleteOutline, Edit } from '@mui/icons-material'
import { DeleteOutline, Undo } from '@mui/icons-material'
import { colors } from '@core'
import { Box } from '@mui/material'
import { Box, Button } from '@mui/material'
import { AllowedUsers, Admin } from './types'

const StyledTableCell = styled(TableCell)(() => ({
Expand All @@ -32,11 +32,7 @@ const StyledTableRow = styled(TableRow)(({ theme }) => ({
},
}))

export const AdministratorsTable = ({
data,
removeAdmin,
addAdmin,
}: AllowedUsers) => {
export const AdministratorsTable = ({ data, removeAdmin }: AllowedUsers) => {
const [isDeleting, setIsDeleting] = useState<boolean>(false)
const [isDeletingRow, setIsDeletingRow] = useState<Admin>()

Expand All @@ -50,11 +46,15 @@ export const AdministratorsTable = ({
}
}

const undoDelete = () => {
setIsDeleting(false)
setIsDeletingRow(undefined)
}

return (
<Box
sx={{
m: 'auto',
mb: 6,
pt: 1,
display: 'flex',
flexDirection: 'column',
Expand All @@ -77,43 +77,49 @@ export const AdministratorsTable = ({
</TableRow>
</TableHead>
<TableBody>
{data.map((admin) => (
<StyledTableRow key={admin.email}>
{data.map((row) => (
<StyledTableRow key={row.email}>
<StyledTableCell>
{admin.name ? admin.name : 'Administrator Name'}
{row.name ? row.name : 'Administrator Name'}
</StyledTableCell>
<StyledTableCell component="th" scope="row">
{admin.email}
{row.email}
</StyledTableCell>
<StyledTableCell
sx={{
width: 150,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
>
<DeleteOutline
color={
isDeleting && admin === isDeletingRow
? 'warning'
: 'action'
}
onClick={() => {
confirmDelete(admin)
}}
{isDeleting && row === isDeletingRow ? (
<StyledTableCell>
<Button
onClick={() => confirmDelete(row)}
sx={{ right: 20 }}
>
Delete
</Button>
<Undo
onClick={() => undoDelete()}
sx={{
'&:hover': { scale: '1.2', cursor: 'pointer' },
}}
/>
</StyledTableCell>
) : (
<StyledTableCell
sx={{
'&:hover': { scale: '1.2', cursor: 'pointer' },
width: 150,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
}}
/>
<Edit
color="action"
onClick={addAdmin}
sx={{
'&:hover': { scale: '1.2', cursor: 'pointer' },
}}
/>
</StyledTableCell>
>
<DeleteOutline
color={'action'}
onClick={() => {
confirmDelete(row)
}}
sx={{
'&:hover': { scale: '1.2', cursor: 'pointer' },
}}
/>
</StyledTableCell>
)}
</StyledTableRow>
))}
</TableBody>
Expand Down
28 changes: 18 additions & 10 deletions frontend/src/modules/Settings/Components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DropdownSelect } from '@core'
import {
Box,
Button,
Grid,
IconButton,
SelectChangeEvent,
Expand All @@ -12,15 +13,13 @@ import {
import MenuItem from '@mui/material/MenuItem'
import AddIcon from '@mui/icons-material/Add'

import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { DASHBOARD_PATH } from '@core'
import { ReactComponent as LogoImg } from '@assets/img/lscicon.svg'
import { AccountMenu } from 'Dashboard/Components/AccountMenu'
import { API_ROOT, COURSE_API, SETTINGS_API } from '@core/Constants'
import { AdministratorsTable } from './AdministratorsTable'
import { Admin } from './types'
import axios from 'axios'
import AddAdminModal from './AddAdminModal'
import { useSettingsValue } from '@context/SettingsContext'
import { useCourseValue } from '@context/CourseContext'

Expand All @@ -30,7 +29,6 @@ export const Settings = () => {
surveyState,
administrators,
semesterAdded,

setCurrRoster,
changeCurrRoster,
changeSurveyAvailability,
Expand All @@ -49,12 +47,23 @@ export const Settings = () => {
String(new Date().getFullYear()).substring(2, 4)
)

const [openAddAdmin, setOpenAddAdmin] = useState(false)
const handleAddAdmin = () => {
setOpenAddAdmin(true)
}
const handleCloseAdmin = () => setOpenAddAdmin(false)

return (
<Box
sx={{
p: '0 5%',
}}
>
<AddAdminModal
open={openAddAdmin}
handleClose={handleCloseAdmin}
addAdmin={addAdmin}
/>
<Box
sx={{
width: '100%',
Expand Down Expand Up @@ -235,11 +244,10 @@ export const Settings = () => {
>
Administrators
</Box>
<AdministratorsTable
data={administrators}
removeAdmin={removeAdmin}
addAdmin={addAdmin}
/>
<AdministratorsTable data={administrators} removeAdmin={removeAdmin} />
<Box my={5} px={5} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => handleAddAdmin()}>Add Admin</Button>
</Box>
</Box>
<Snackbar
open={semesterAdded}
Expand Down
Loading

0 comments on commit 04390cb

Please sign in to comment.