Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions api/main_endpoints/routes/OfficeAccessCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {
checkIfCardExists,
generateAlias,
deleteCard,
editAlias,
} = require('../util/OfficeAccessCard.js');
const AuditLogActions = require('../util/auditLogActions.js');
const AuditLog = require('../models/AuditLog.js');
Expand Down Expand Up @@ -82,10 +83,10 @@ router.get('/verify', async (req, res) => {

if (apiKey !== API_KEY) {
writeLogToClient(req.method, {
statusCode: UNAUTHORIZED,
statusCode: FORBIDDEN,
message: `Invalid API key: ${apiKey}`,
});
return res.sendStatus(UNAUTHORIZED);
return res.sendStatus(FORBIDDEN);
}

const cardExists = await checkIfCardExists({ cardBytes });
Expand Down Expand Up @@ -209,6 +210,56 @@ router.post('/getAllCards', async (req, res) => {
}
});

router.post('/edit', async (req, res) => {
const decoded = await decodeToken(req, membershipState.OFFICER);
if (decoded.status !== OK) {
return res.sendStatus(decoded.status);
}

const { _id, alias } = req.body;

const required = [
{ value: _id && /^[0-9a-fA-F]{24}$/.test(_id) ? _id : null, title: 'Valid, alphanumeric Card ID', },
{ value: alias?.trim(), title: 'New card alias', },
];

const missingValue = required.find(({ value }) => !value);
if (missingValue) {
writeLogToClient(req.method, {
statusCode: BAD_REQUEST,
message: `${missingValue.title} missing from request`,
});
return res.status(BAD_REQUEST).send(`${missingValue.title} missing from request`);
}

try {
const updatedCard = await editAlias(_id, alias);

if (!updatedCard) {
return res.sendStatus(NOT_FOUND);
}

// Log the edit action
AuditLog.create({
userId: decoded.token._id,
action: AuditLogActions.EDIT_CARD,
details: {
newAlias: alias,
_id,
}
});

logger.info(`Card alias updated successfully for card ID: ${_id}`);
return res.status(OK).json({
message: 'Card alias updated successfully',
card: updatedCard
});
} catch (error) {
logger.error('Error updating card alias: ', error);
return res.status(SERVER_ERROR).send('Error updating card alias');
}
});

router.get('/listen', async (req, res) => {
const decoded = await decodeToken(req, membershipState.OFFICER);
if (decoded.status !== OK) {
Expand Down
32 changes: 29 additions & 3 deletions api/main_endpoints/util/OfficeAccessCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ function checkIfCardExists({ cardBytes = null, alias = null } = {}) {
return resolve(false);
}
if (!result) {
const { description } = body;
logger.info(`Card:${description} not found in the database`);
const description = cardBytes !== null ? cardBytes : alias;
logger.info(`Card: ${description} not found in the database`);
}
return resolve(result); // return the document
});
Expand Down Expand Up @@ -87,4 +87,30 @@ function deleteCard(alias) {
});
}

module.exports = { checkIfCardExists, generateAlias, deleteCard };
function editAlias(_id, newAlias) {
return new Promise((resolve) => {
try {
OfficeAccessCard.findByIdAndUpdate(
_id,
{ $set: { alias: newAlias } },
{ new: true, useFindAndModify: false },
(error, result) => {
if (error) {
logger.error('editAlias got an error querying mongodb: ', error);
return resolve(false);
}
if (!result) {
logger.info(`Card with id ${_id} not found in the database`);
return resolve(false);
}
return resolve(result);
}
);
} catch (error) {
logger.error('editAlias caught an error: ', error);
return resolve(false);
}
});
}

module.exports = { checkIfCardExists, generateAlias, deleteCard, editAlias };
1 change: 1 addition & 0 deletions api/main_endpoints/util/auditLogActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AuditLogActions = {
VERIFY_CARD: 'VERIFY_CARD',
ADD_CARD: 'ADD_CARD',
DELETE_CARD: 'DELETE_CARD',
EDIT_CARD: 'EDIT_CARD',
};

module.exports = AuditLogActions;
25 changes: 25 additions & 0 deletions src/APIFunctions/CardReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,28 @@ export async function deleteCardFromDb(token, alias) {
}
return status;
}

export async function editCardAlias(token, _id, alias) {
let status = new ApiResponse();
try {
const url = new URL('/api/OfficeAccessCard/edit', BASE_API_URL);
const res = await fetch(url.href, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ _id, alias }),
});
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;
}
114 changes: 102 additions & 12 deletions src/Pages/CardReader/CardReader.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect } from 'react';
import { BASE_API_URL } from '../../Enums';
import { useSCE } from '../../Components/context/SceContext';
import { getAllCardsFromDb, deleteCardFromDb } from '../../APIFunctions/CardReader';
import { getAllCardsFromDb, deleteCardFromDb, editCardAlias } from '../../APIFunctions/CardReader';
import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal';
import { trashcanSymbol } from '../Overview/SVG';
import { trashcanSymbol, pencilSymbol, checkSymbol, cancelSymbol } from '../Overview/SVG';

const header = [
'Time'.padEnd(29),
Expand All @@ -17,10 +17,13 @@ const header = [
export default function CardReader() {
const { user } = useSCE();
const token = user.token;

const [logs, setLogs] = useState([]);
const [cards, setCards] = useState([]);
const [toggleDelete, setToggleDelete] = useState(false);
const [cardToDelete, setCardToDelete] = useState({});
const [editingCardId, setEditingCardId] = useState(null);
const [editedAlias, setEditedAlias] = useState('');
const [tab, setTab] = useState(() => {
const params = new URLSearchParams(window.location.search);
return params.get('tab') || 'registry';
Expand All @@ -44,9 +47,9 @@ export default function CardReader() {

const getColumnClassName = (columnName) => {
let className = 'px-6 py-3 whitespace-nowrap ';
if(columnName === 'lastVerifiedAt' | columnName === 'registrationDate'){
if (['lastVerifiedAt', 'registrationDate'].includes(columnName)) {
className += 'hidden md:table-cell ';
} else if (columnName === 'verifiedCount'){
} else if (columnName === 'verifiedCount') {
className += 'hidden lg:table-cell';
}
return className;
Expand Down Expand Up @@ -89,12 +92,78 @@ export default function CardReader() {
setCardToDelete(card);
}

function handleEditClick(card) {
if (editingCardId === card._id) {
setEditingCardId(null);
setEditedAlias('');
} else {
setEditingCardId(card._id);
setEditedAlias(card.alias);
}
}

async function handleSaveEdit(cardId) {
if (!editedAlias.trim()) {
return; // Don't save empty alias
}

try {
const response = await editCardAlias(token, cardId, editedAlias.trim());
if (!response.error) {
// Refetch all cards from database to ensure UI matches server reality
await getAllCards();
setEditingCardId(null);
setEditedAlias('');
}
} catch (error) {
setLogs(
(currLogs) => [
'[error] unable to update card alias, check browser logs: \n' + error,
...currLogs,
]
);
}
}

function handleEditKeyDown(key) {
if (key === 'Enter') {
handleSaveEdit(editingCardId);
} else if (key === 'Escape') {
setEditingCardId(null);
setEditedAlias('');
}
}

function renderInputOrAlias(card) {
if (editingCardId === card._id) {
return (
<input
type='text'
value={editedAlias}
onChange={(e) => setEditedAlias(e.target.value)}
onKeyDown={(e) => handleEditKeyDown(e.key)}
className='bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded text-gray-700 dark:text-white font-medium m-0 px-1 py-0 focus:outline-none focus:ring-1 focus:ring-blue-500'
style={{ width: '16ch' }}
autoFocus
/>
);
}
return (
<div style={{ width: '16ch', display: 'flex', justifyContent: 'center', overflow: 'hidden' }}>
<span style={{ textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
{card.alias}
</span>
</div>
);
}

function CardEntry({ card }) {
const isEditing = editingCardId === card._id;
return (
<tr key={card._id} className='bg-white border-b dark:bg-gray-800 dark:border-gray-700'>
<td key='alias' className=''>
<div className='px-6 py-4 font-medium text-gray-700 whitespace-nowrap dark:text-white'>
{card.alias}
{renderInputOrAlias(card)}
</div>
</td>
<td key='createdAt' className='hidden md:table-cell'>
Expand All @@ -113,12 +182,33 @@ export default function CardReader() {
</div>
</td>
<td>
<button
className='p-2 hover:bg-gray-200 dark:hover:bg-white/30 rounded-xl'
onClick={() => handleDeleteClick(card)}
>
{trashcanSymbol('#e64539')}
</button>
<div className='flex space-x-2 w-32'>
<button
className={`p-2 hover:bg-gray-200 dark:hover:bg-white/30 rounded-xl ${!isEditing ? 'invisible' : ''}`}
onClick={() => handleSaveEdit(card._id)}
title='Save changes'
>
{checkSymbol('#22c55e')}
</button>
<button
className='p-2 hover:bg-gray-200 dark:hover:bg-white/30 rounded-xl'
onClick={() => handleEditClick(card)}
title={isEditing ? 'Cancel edit' : 'Edit alias'}
>
{isEditing ? (
cancelSymbol('#ef4444')
) : (
pencilSymbol('#6b7280')
)}
</button>
<button
className='p-2 hover:bg-gray-200 dark:hover:bg-white/30 rounded-xl'
onClick={() => handleDeleteClick(card)}
title='Delete card'
>
{trashcanSymbol('#e64539')}
</button>
</div>
</td>
</tr>
);
Expand Down
26 changes: 26 additions & 0 deletions src/Pages/Overview/SVG.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ export function trashcanSymbol(color = 'black') {
);
}

export function pencilSymbol(color = '#6b7280') {
return (
<svg width='20' height='20' viewBox='0 0 24 24' fill='none' stroke={color} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'>
<path d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7' />
<path d='m18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z' />
</svg>
);
}

export function checkSymbol(color = '#22c55e') {
return (
<svg width='20' height='20' viewBox='0 0 24 24' fill='none' stroke={color} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'>
<polyline points='20,6 9,17 4,12'></polyline>
</svg>
);
}

export function cancelSymbol(color = '#ef4444') {
return (
<svg width='20' height='20' viewBox='0 0 24 24' fill='none' stroke={color} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'>
<line x1='18' y1='6' x2='6' y2='18'></line>
<line x1='6' y1='6' x2='18' y2='18'></line>
</svg>
);
}

export function copyIcon(className, onClick) {
return (
<svg width="24" height="24" viewBox="0 0 512 512" onClick={onClick}>
Expand Down
Loading