diff --git a/api/main_endpoints/models/OfficeAccessCard.js b/api/main_endpoints/models/OfficeAccessCard.js index fb612711a..e6dc91968 100644 --- a/api/main_endpoints/models/OfficeAccessCard.js +++ b/api/main_endpoints/models/OfficeAccessCard.js @@ -14,7 +14,7 @@ const OfficeAccessCardSchema = new Schema( }, verifiedCount:{ type:Number, - default:0 + default:1 }, lastVerified:{ type:Date, diff --git a/api/main_endpoints/routes/OfficeAccessCard.js b/api/main_endpoints/routes/OfficeAccessCard.js index 930c66132..00218d07f 100644 --- a/api/main_endpoints/routes/OfficeAccessCard.js +++ b/api/main_endpoints/routes/OfficeAccessCard.js @@ -4,6 +4,7 @@ const { SERVER_ERROR, NOT_FOUND, OK, + FORBIDDEN, } = require('../../util/constants').STATUS_CODES; const express = require('express'); const router = express.Router(); @@ -12,7 +13,13 @@ const OfficeAccessCard = require('../models/OfficeAccessCard.js'); const logger = require('../../util/logger'); const { officeAccessCard = {} } = require('../../config/config.json'); const { API_KEY = 'NOTHING_REALLY' } = officeAccessCard; -const { decodeTokenFromBodyOrQuery } = require('../util/token-functions.js'); +const { + decodeTokenFromBodyOrQuery, + decodeToken, + checkIfTokenSent, + checkIfTokenValid +} = require('../util/token-functions.js'); +const ROWS_PER_PAGE = 25; router.use(bodyParser.json()); @@ -20,21 +27,20 @@ function checkIfCardExists(cardBytes) { return new Promise((resolve) => { try { OfficeAccessCard.findOneAndUpdate( - { cardBytes:cardBytes}, + { cardBytes }, { $inc: { verifiedCount: 1 }, $set: { lastVerified: Date.now() } }, { - useFindAndModify: false, new:true, upsert:false + useFindAndModify: false, new: true, upsert: false } , (error, result) => { if (error) { logger.error('checkIfCardExists got an error querying mongodb: ', error); return resolve(false); } - if(!result){ + if (!result) { logger.info(`Card:${cardBytes} not found in the database`); - return resolve(false); } return resolve(!!result); }); @@ -56,7 +62,7 @@ function deleteCard(cardBytes) { return resolve(false); } if (!result) { - logger.info(`Card:${cardBytes} not found in the database`); + logger.info(`Card ${cardBytes} not found in the database`); } return resolve(!!result); } @@ -70,18 +76,33 @@ function deleteCard(cardBytes) { let clients = []; -const defaultResponse = { - cardWasAdded: false, +const defaultGetResponse = { message: 'Card authorized!', endpoint: '/verify', + requestType: 'GET', }; -const writeRequestResponse = ({ statusCode, ...rest }) => { - const response = { - statusCode, - ...defaultResponse, - ...rest, - }; +const defaultPostResponse = { + message: 'Card deleted!', + endpoint: '/delete', + requestType: 'POST', +}; + +const writeLogToClient = (requestType, { statusCode, ...rest }) => { + let response = {}; + if (requestType === 'GET') { + response = { + statusCode, + ...defaultGetResponse, + ...rest, + }; + } else { + response = { + statusCode, + ...defaultPostResponse, + ...rest, + }; + } clients.forEach(client => { client.res.write(`data: ${JSON.stringify(response)}\n\n`); }); @@ -98,7 +119,7 @@ router.get('/verify', async (req, res) =>{ const missingValue = required.find(({ value }) => !value); if (missingValue) { - writeRequestResponse({ statusCode: BAD_REQUEST, message: `${missingValue.title} missing from request` }); + writeLogToClient(req.method, { statusCode: BAD_REQUEST, message: `${missingValue.title} missing from request` }); return res.status(BAD_REQUEST).send(` ${missingValue.title} missing from request`); } @@ -108,7 +129,7 @@ router.get('/verify', async (req, res) =>{ const cardExists = await checkIfCardExists(cardBytes); if (cardExists) { - writeRequestResponse({ statusCode: OK }); + writeLogToClient(req.method, { statusCode: OK }); return res.sendStatus(OK); } // if a card doesnt exist and we arent trying @@ -116,22 +137,20 @@ router.get('/verify', async (req, res) =>{ // to verify a card, and that card isnt found. // therefore return a non OK status if (!add) { - writeRequestResponse({ statusCode: NOT_FOUND, message: 'Card not found' }); + writeLogToClient(req.method, { statusCode: NOT_FOUND, message: 'Card not found' }); return res.sendStatus(NOT_FOUND); } - + // if we reached here, the card does not exist and is trying to be added try { - if (add) { - logger.info('adding a new card'); - await new OfficeAccessCard({ - cardBytes - }).save(); - writeRequestResponse({ statusCode: OK, message: 'Card added!', endpoint: '/verify?add=1' }); - return res.sendStatus(OK); - } + logger.info('adding a new card'); + await new OfficeAccessCard({ + cardBytes, + }).save(); + writeLogToClient(req.method, { statusCode: OK, message: 'Card added!', endpoint: '/verify?add=1' }); + return res.sendStatus(OK); } catch (error) { logger.error('Error creating OfficeAccessCard: ', error); - writeRequestResponse({ + writeLogToClient(req.method, { statusCode: SERVER_ERROR, endpoint: '/verify?add=1', message: `Error creating Office AccessCard: ${error}` @@ -140,44 +159,67 @@ router.get('/verify', async (req, res) =>{ } }); -router.get('/delete', async (req, res) => { - if (!await decodeTokenFromBodyOrQuery(req)) { +router.post('/delete', async (req, res) => { + if (!await decodeToken(req)) { return res.sendStatus(UNAUTHORIZED); } - const { cardBytes } = req.query; + const { cardBytes } = req.body; if (!cardBytes) { - writeRequestResponse({ + writeLogToClient(req.method, { statusCode: BAD_REQUEST, - endpoint: '/delete', - message: 'cardBytes missing from request' + message: 'cardBytes missing from request', + requestType: req.method, }); return res.sendStatus(BAD_REQUEST); } if (!await checkIfCardExists(cardBytes)) { logger.info('Card does not exist'); - writeRequestResponse({ + writeLogToClient(req.method, { statusCode: NOT_FOUND, - endpoint: '/delete', message: 'Card does not exist', + requestType: req.method, }); return res.sendStatus(NOT_FOUND); } - const tryDeleteCard = await deleteCard(cardBytes); - if (!tryDeleteCard) { - logger.info('Error deleting card'); - writeRequestResponse({ - statusCode: SERVER_ERROR, - endpoint: '/delete', - message: 'Error deleting card' + if (await deleteCard(cardBytes)) { // successful + logger.info('Successfully deleted card'); + writeLogToClient(req.method, { + statusCode: OK, + message: 'Card deleted!', }); + return res.sendStatus(OK); + } + writeLogToClient({ + statusCode: SERVER_ERROR, + message: 'Error deleting card', + }); + return res.sendStatus(SERVER_ERROR); +}); + +router.post('/getAllCards', async (req, res) => { + if (!checkIfTokenSent(req)) { + return res.sendStatus(FORBIDDEN); + } else if (!checkIfTokenValid(req)) { + return res.sendStatus(UNAUTHORIZED); + } + + const skip = Math.max(Number(req.body.page) || 0, 0) * ROWS_PER_PAGE; + + try { + const total = await OfficeAccessCard.count({}); + const items = await OfficeAccessCard.find({}, {}, { skip, limit: ROWS_PER_PAGE }); + return res.status(OK).send({ + items, + total, + rowsPerPage: ROWS_PER_PAGE, + }); + } catch (error) { + logger.error('Error fetching cards: ', error); return res.sendStatus(SERVER_ERROR); } - logger.info('Successfully deleted card'); - writeRequestResponse({ statusCode: OK, endpoint: '/delete', message: 'Card deleted!' }); - return res.sendStatus(OK); }); router.get('/listen', async (req, res) => { diff --git a/src/APIFunctions/CardReader.js b/src/APIFunctions/CardReader.js new file mode 100644 index 000000000..cec5ea517 --- /dev/null +++ b/src/APIFunctions/CardReader.js @@ -0,0 +1,57 @@ +import { ApiResponse } from './ApiResponses'; +import { BASE_API_URL } from '../Enums'; + +export async function getAllCardsFromDb({ + token, + page = null, +}) { + let status = new ApiResponse(); + try { + const url = new URL('/api/OfficeAccessCard/getAllCards', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + body: JSON.stringify({ + page, + }), + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + } + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} + +export async function deleteCardFromDb(token, cardBytes) { + let status = new ApiResponse(); + try { + const url = new URL('/api/OfficeAccessCard/delete', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ cardBytes, }), + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} diff --git a/src/APIFunctions/Messaging.js b/src/APIFunctions/Messaging.js index 7fb04f0bf..d29f6e5d5 100644 --- a/src/APIFunctions/Messaging.js +++ b/src/APIFunctions/Messaging.js @@ -25,7 +25,7 @@ export async function sendMessage(id, token, message) { } } catch(err) { status.error = true; - status.responseData = error; + status.responseData = err; } return status; } diff --git a/src/Pages/CardReader/CardReader.js b/src/Pages/CardReader/CardReader.js index 0e3e5e9fe..a2437e5ba 100644 --- a/src/Pages/CardReader/CardReader.js +++ b/src/Pages/CardReader/CardReader.js @@ -1,30 +1,123 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { BASE_API_URL } from '../../Enums'; import { useUser } from '../../Components/context/UserContext'; +import { getAllCardsFromDb, deleteCardFromDb } from '../../APIFunctions/CardReader'; +import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal'; +import { trashcanSymbol } from '../Overview/SVG'; + +const header = [ + 'TIMESTAMP'.padEnd(30), + 'TYPE'.padEnd(12), + 'ENDPOINT'.padEnd(20), + 'STATUS CODE'.padEnd(15), + 'MESSAGE\n' +].join(''); export default function CardReader() { - const [logs, setLogs] = useState([]); - const [error, setError] = useState(''); const { user } = useUser(); const token = user.token; + const [logs, setLogs] = useState([]); + const [cards, setCards] = useState([]); + const [toggleDelete, setToggleDelete] = useState(false); + const [cardToDelete, setCardToDelete] = useState({}); + const [tab, setTab] = useState(() => { + const params = new URLSearchParams(window.location.search); + return params.get('tab') || 'registry'; + }); + const [connected, setConnected] = useState(true); + const [connectionStatusText, setConnectionStatusText] = useState(''); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const [paginationText, setPaginationText] = useState(''); + const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); + + function buildLog(data) { + const date = new Date().toISOString().padEnd(30); + const requestType = data.requestType.padEnd(12); + const endpoint = data.endpoint.padEnd(20); + const statusCode = String(data.statusCode).padEnd(15); + return [date, requestType, endpoint, statusCode, data.message].join(''); + } + + async function getAllCards() { + setLoading(true); + const apiResponse = await getAllCardsFromDb({ + token, + page, + }); + if (!apiResponse.error) { + setCards(apiResponse.responseData.items); + setTotal(apiResponse.responseData.total); + setRowsPerPage(apiResponse.responseData.rowsPerPage); + } + setLoading(false); + } - const buildLog = (data) => { - let date = new Date().toISOString().padEnd(30, ' '); - let endpoint = data.endpoint.padEnd(20, ' '); - let statusCode = String(data.statusCode).padEnd(8, ' '); - return [date, endpoint, statusCode, data.message].join(''); - }; + function handleTabChange(newTab) { + setTab(newTab); + const params = new URLSearchParams(window.location.search); + params.set('tab', newTab); + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.pushState({}, '', newUrl); + } + + function handleDeleteClick(card) { + setToggleDelete(!toggleDelete); + setCardToDelete(card); + } + + function CardEntry({ card }) { + return ( + + +
+ {card.cardBytes} +
+ + +
+ {card.createdAt} +
+ + +
+ {card.lastVerified} +
+ + +
+ {card.verifiedCount} +
+ + + + + + ); + } useEffect(() => { + getAllCards(); const url = new URL('/api/OfficeAccessCard/listen', BASE_API_URL); url.searchParams.append('token', token); const eventSource = new EventSource(url.href); eventSource.onmessage = (event) => { try { - let data = JSON.parse(event.data); - let newLog = buildLog(data); - setLogs(currLogs => [newLog, ...currLogs]); // prepend the new log - } catch (error) { + const data = JSON.parse(event.data); + const url = new URL(data.endpoint, window.location.origin); + // if card is verified or added successfully, query the DB again to get latest card data + if (url.pathname === '/verify' && data.statusCode === 200) { + getAllCards(); + } + const newLog = buildLog(data); + setLogs(currLogs => [newLog, ...currLogs]); + } catch (err) { // if the message sent from error cannot be parsed setLogs( (currLogs) => [ '[error] unable to format response, check browser logs', @@ -34,25 +127,164 @@ export default function CardReader() { } }; - eventSource.onerror = () => { - setError('Error connecting to SSE'); + eventSource.onerror = () => { // if the connection to the event source encounters error + setLogs( + (currLogs) => [ + 'EventSource didn\'t work, check browser logs', + ...currLogs + ] + ); + setConnected(false); }; return () => { eventSource.close(); }; - }, []); + useEffect(() => { + if (connected) { + setConnectionStatusText('Listening for card reader activity...'); + return; + } + setConnectionStatusText('Client disconnected. Please reload to reconnect.'); + }, [connected]); + + useEffect(() => { + const amountOfCardsOnCurrentPage = Math.min((page + 1) * rowsPerPage, cards.length); + const pageOffset = page * rowsPerPage; + const startingElementNumber = (page * rowsPerPage) + 1; + const endingElementNumber = amountOfCardsOnCurrentPage + pageOffset; + setPaginationText( + <> +

+ {startingElementNumber} - {endingElementNumber} / {total} +

+

+ Showing {startingElementNumber} to {endingElementNumber} of {total} results +

+ + ); + }, [page, rowsPerPage, cards, total]); + + useEffect(() => { + getAllCards(); + }, [page]); + + function maybeRenderPagination() { + const amountofCardsOnCurrentPage = Math.min((page + 1) * rowsPerPage, cards.length); + const pageOffset = page * rowsPerPage; + const endingElementNumber = amountofCardsOnCurrentPage + pageOffset; + if (cards.length) { + return ( + + ); + } + return <>; + } + + function getComponentFromTabChoice(tab) { + if (tab === 'registry') { + return ( +
+ { + await deleteCardFromDb(token, cardToDelete.cardBytes); + await getAllCards(); + setToggleDelete(!toggleDelete); + }, + open: toggleDelete + } + } /> +
+ + + + {[ + { title: 'Card Bytes', columnName: 'cardBytes' }, + { title: 'Registration Date', columnName: 'registrationDate' }, + { title: 'Last Verified At', columnName: 'lastVerifiedAt' }, + { title: 'Verified Count', columnName: 'verifiedCount' } + ].map(({ title }) => ( + + ))} + + + + {cards.map(card => )} + +
+
+ {title} +
+
+ {maybeRenderPagination()} +
+
+ ); + } + return ( +
+

+ {connectionStatusText} +

+
+          {header}
+          {logs.join('\n')}
+        
+
+ ); + } + return ( -
-

SCE Card Reader Activity

-
-        {logs.join('\n')}
-      
- {error && -

{error}

- } +
+

SCE Card Reader Page

+
This webpage manages RFID cards used to unlock the office door in the SCE room
+
+ + {/* spacer to differentiate between the two options */} +
 | 
+ +
+ {getComponentFromTabChoice(tab)}
); }