Admin Features & Message Logs
I wanted the app to have some administrative capabilities, especially for monitoring message edits and deletions. This helps maintain accountability and provides a way to recover information if needed.
Admin Privileges System
The admin privileges system is pretty straightforward. I store an isAdmin flag in the user object in Firebase:
/users/{userId}: {
// ... other user data
"isAdmin": true
}
Then I added a helper function in the ChatContext to check if the current user has admin privileges:
// In ChatContext.jsx
const isCurrentUserAdmin = useCallback(() => {
return user && userData && userData.isAdmin === true;
}, [user, userData]);
This isn't exactly Fort Knox security-wise, but it's good enough for our purposes. In a production app, I'd use Firebase security rules to restrict certain operations based on admin status.
Message Logs System
The message logs system tracks all edits and deletions in a dedicated collection:
/logs/{chatId}/{logId}: {
"messageId": "abc123",
"type": "EDIT", // or "DELETE"
"performedBy": "userId",
"timestamp": 1650120050000,
"originalContent": "Hello there",
"newContent": "Hello everyone" // Only for EDIT logs
}
Implementing the Logs in ChatContext
I added several new functions to the ChatContext to handle logs:
// src/contexts/ChatContext.jsx
const [logs, setLogs] = useState([]);
// Function to record a log when a message is edited
const logMessageEdit = async (chatId, messageId, originalContent, newContent) => {
try {
const logRef = push(ref(db, `logs/${chatId}`));
await set(logRef, {
messageId,
type: 'EDIT',
performedBy: user.uid,
performedByName: user.displayName,
timestamp: serverTimestamp(),
originalContent,
newContent
});
console.log('Edit log created');
} catch (error) {
console.error('Error creating edit log:', error);
}
};
// Function to record a log when a message is deleted
const logMessageDelete = async (chatId, messageId, originalContent) => {
try {
const logRef = push(ref(db, `logs/${chatId}`));
await set(logRef, {
messageId,
type: 'DELETE',
performedBy: user.uid,
performedByName: user.displayName,
timestamp: serverTimestamp(),
originalContent
});
console.log('Delete log created');
} catch (error) {
console.error('Error creating delete log:', error);
}
};
// Function to fetch logs for a specific chat
const fetchChatLogs = useCallback(async (chatId) => {
if (!isCurrentUserAdmin() || !chatId) {
return [];
}
try {
const logsRef = ref(db, `logs/${chatId}`);
const snapshot = await get(logsRef);
if (!snapshot.exists()) {
return [];
}
const logsData = snapshot.val();
const logsList = Object.keys(logsData).map(key => ({
id: key,
...logsData[key]
}));
// Sort logs by timestamp (newest first)
return logsList.sort((a, b) => b.timestamp - a.timestamp);
} catch (error) {
console.error('Error fetching logs:', error);
return [];
}
}, [isCurrentUserAdmin]);
// Updated edit message function to include logging
const editMessage = async (chatId, messageId, newContent) => {
if (!chatId || !messageId || !newContent.trim()) return;
try {
const messageRef = ref(db, `messages/${chatId}/${messageId}`);
const snapshot = await get(messageRef);
if (!snapshot.exists()) {
throw new Error('Message does not exist');
}
const messageData = snapshot.val();
// Check if the user can edit this message
if (messageData.sender !== user.uid && !isCurrentUserAdmin()) {
throw new Error('You cannot edit this message');
}
// Log the edit
await logMessageEdit(chatId, messageId, messageData.content, newContent);
// Update the message
await update(messageRef, {
content: newContent,
edited: true,
editedAt: serverTimestamp()
});
return true;
} catch (error) {
console.error('Error editing message:', error);
return false;
}
};
// Updated delete message function to include logging
const deleteMessage = async (chatId, messageId) => {
if (!chatId || !messageId) return;
try {
const messageRef = ref(db, `messages/${chatId}/${messageId}`);
const snapshot = await get(messageRef);
if (!snapshot.exists()) {
throw new Error('Message does not exist');
}
const messageData = snapshot.val();
// Check if the user can delete this message
if (messageData.sender !== user.uid && !isCurrentUserAdmin()) {
throw new Error('You cannot delete this message');
}
// Log the deletion
await logMessageDelete(chatId, messageId, messageData.content);
// Mark as deleted rather than actually deleting
await update(messageRef, {
deleted: true,
content: 'This message has been deleted',
deletedAt: serverTimestamp()
});
return true;
} catch (error) {
console.error('Error deleting message:', error);
return false;
}
};
LogViewer Component
I created a simple UI component to view the logs:
// src/components/chat/LogViewer.jsx
import { useState, useEffect } from 'react';
import { useChat } from '../../contexts/ChatContext';
import styles from './LogViewer.module.css';
export default function LogViewer({ chatId, onClose }) {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const { fetchChatLogs } = useChat();
useEffect(() => {
const loadLogs = async () => {
setLoading(true);
const chatLogs = await fetchChatLogs(chatId);
setLogs(chatLogs);
setLoading(false);
};
loadLogs();
}, [chatId, fetchChatLogs]);
// Format timestamp to readable date
const formatDate = (timestamp) => {
if (!timestamp) return 'Unknown';
const date = new Date(timestamp);
return date.toLocaleString();
};
return (
<div className={styles.logViewer}>
<div className={styles.header}>
<h2>Message Logs</h2>
<button
className={styles.closeButton}
onClick={onClose}
aria-label="Close log viewer"
>
×
</button>
</div>
{loading ? (
<div className={styles.loading}>Loading logs...</div>
) : logs.length === 0 ? (
<div className={styles.noLogs}>No logs found for this chat</div>
) : (
<div className={styles.logList}>
{logs.map(log => (
<div key={log.id} className={styles.logItem}>
<div className={styles.logHeader}>
<span className={styles.logType}>
{log.type === 'EDIT' ? '✏️ Edit' : '🗑️ Delete'}
</span>
<span className={styles.logTime}>{formatDate(log.timestamp)}</span>
</div>
<div className={styles.logUser}>
<strong>By:</strong> {log.performedByName || 'Unknown user'}
</div>
<div className={styles.logContent}>
<div className={styles.originalContent}>
<strong>Original:</strong> {log.originalContent}
</div>
{log.type === 'EDIT' && (
<div className={styles.newContent}>
<strong>Changed to:</strong> {log.newContent}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
);
}
Adding the "View Logs" Button
I added a button to the chat header that's only visible to admins:
// In ChatArea.jsx
const renderChatHeader = () => {
return (
<div className={styles.header}>
{/* Other header elements */}
{isCurrentUserAdmin() && (
<button
className={styles.viewLogsButton}
onClick={() => setShowLogViewer(true)}
title="View message logs"
>
📋 Logs
</button>
)}
</div>
);
};
// Later in the component
return (
<div className={styles.chatArea}>
{renderChatHeader()}
{/* Rest of the component */}
{showLogViewer && (
<LogViewer
chatId={currentChat.id}
onClose={() => setShowLogViewer(false)}
/>
)}
</div>
);
Styling the Log Viewer
I wanted the LogViewer to have a distinct "admin" feel:
/* LogViewer.module.css */
.logViewer {
position: absolute;
top: 0;
right: 0;
width: 350px;
height: 100%;
background-color: #f8f9fa;
border-left: 1px solid #dee2e6;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
z-index: 10;
overflow-y: auto;
padding: 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #dee2e6;
}
.header h2 {
margin: 0;
color: #343a40;
}
.closeButton {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
}
.logList {
display: flex;
flex-direction: column;
gap: 1rem;
}
.logItem {
background-color: white;
border-radius: 8px;
padding: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.logHeader {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.logType {
font-weight: 500;
}
.logTime {
font-size: 0.8rem;
color: #6c757d;
}
.logUser {
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.logContent {
background-color: #f1f3f5;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.9rem;
}
.originalContent {
margin-bottom: 0.5rem;
}
.originalContent strong,
.newContent strong {
color: #495057;
}
Technical Challenges
Deciding What to Log
I had to think carefully about what information to include in the logs. Too much, and it would be unwieldy; too little, and it wouldn't be useful. I settled on:
- Who made the change
- When it happened
- The original content
- The new content (for edits)
This gives admins enough context without overwhelming them with details.
Soft Deletes vs. Hard Deletes
I went with soft deletes (marking messages as deleted rather than actually removing them) for a few reasons:
- It preserves the conversation flow
- It allows for potential message recovery
- It's consistent with how other chat apps handle deleted messages
Security Considerations
I wanted to make sure that only admins could see the logs. I implemented this at multiple levels:
- Only showing the "View Logs" button to admins
- Checking admin status before fetching logs in the
fetchChatLogsfunction - Planning to add Firebase security rules to restrict access to the logs collection
Future Improvements
If I had more time, I'd enhance this feature with:
- More detailed filtering options for logs (by user, action type, date range)
- Ability to revert message edits or restore deleted messages
- Export logs to CSV/PDF for record-keeping
- More detailed admin analytics (message frequency, peak usage times)
- Additional log types (user joins/leaves, file uploads, etc.)
Working on this feature has taught me a lot about balancing transparency and privacy. While we want to track changes for moderation purposes, we also need to respect user privacy and not log unnecessary information.