Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
44 changes: 35 additions & 9 deletions server/db/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { redisConnection } from './connection.js';
import { decrypt } from '../utils/encryption.js';
import { getAdminIds, isAdmin } from '../middleware/admin.js';
import { getActiveUsersForSession } from "../websockets/sessionUsersWebsocket.js"; // Update import
import { getActiveUsersForSession } from "../websockets/sessionUsersWebsocket.js";
import { getUserRoles } from "./roles.js";

type RawUser = {
Expand Down Expand Up @@ -321,7 +321,7 @@
let rolePermissions = null;
try {
if (user.role_permissions) {
rolePermissions = typeof user.role_permissions === 'string'

Check warning on line 324 in server/db/admin.ts

View workflow job for this annotation

GitHub Actions / build

'rolePermissions' is assigned a value but never used

Check warning on line 324 in server/db/admin.ts

View workflow job for this annotation

GitHub Actions / build

'rolePermissions' is assigned a value but never used
? JSON.parse(user.role_permissions)
: user.role_permissions;
}
Expand Down Expand Up @@ -395,10 +395,11 @@
}
}

export async function getAdminSessions() {
export async function getAdminSessions(page = 1, limit = 100, search = '') {
try {
// Get all sessions with user info
const sessions = await mainDb
const offset = (page - 1) * limit;

let query = mainDb
.selectFrom('sessions as s')
.leftJoin('users as u', 's.created_by', 'u.id')
.select([
Expand All @@ -413,10 +414,28 @@
'u.discriminator',
'u.avatar'
])
.orderBy('s.created_at', 'desc')
.execute();
.orderBy('s.created_at', 'desc');

if (search && search.trim()) {
const searchTerm = `%${search.trim()}%`;
query = query.where((eb) =>
eb.or([
eb('s.session_id', 'ilike', searchTerm),
eb('s.airport_icao', 'ilike', searchTerm),
eb('u.username', 'ilike', searchTerm),
eb('s.created_by', 'ilike', searchTerm)
])
);
}

const countQuery = query.clearSelect().clearOrderBy().select(({ fn }) => fn.countAll().as('count'));
const countResult = await countQuery.executeTakeFirst();
const total = Number(countResult?.count) || 0;
const pages = Math.ceil(total / limit);

const sessions = await query.limit(limit).offset(offset).execute();

const sessionsWithFlights = await Promise.all(
const sessionsWithDetails = await Promise.all(
sessions.map(async (session) => {
let flight_count = 0;
try {
Expand All @@ -439,14 +458,21 @@
})
);

return sessionsWithFlights;
return {
sessions: sessionsWithDetails,
pagination: {
page,
limit,
total,
pages
}
};
} catch (error) {
console.error('Error fetching admin sessions:', error);
throw error;
}
}


export async function syncUserSessionCounts() {
try {
const sessionCounts = await mainDb
Expand Down
91 changes: 91 additions & 0 deletions server/db/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { mainDb } from "./connection.js";
import { sql } from "kysely";

export async function getAllFeedback() {
try {
const feedback = await mainDb
.selectFrom('feedback')
.leftJoin('users', 'feedback.user_id', 'users.id')
.select([
'feedback.id',
'feedback.user_id',
'feedback.username',
'feedback.rating',
'feedback.comment',
'feedback.created_at',
'feedback.updated_at',
'users.avatar',
])
.orderBy('feedback.created_at', 'desc')
.execute();
return feedback;
} catch (error) {
console.error('Error fetching feedback:', error);
throw error;
}
}

export async function addFeedback({
userId,
username,
rating,
comment,
}: {
userId: string;
username: string;
rating: number;
comment?: string;
}) {
try {
const [feedback] = await mainDb
.insertInto('feedback')
.values({
id: sql`DEFAULT`,
user_id: userId,
username,
rating,
comment,
})
.returningAll()
.execute();
return feedback;
} catch (error) {
console.error('Error adding feedback:', error);
throw error;
}
}

export async function deleteFeedback(id: number) {
try {
const [feedback] = await mainDb
.deleteFrom('feedback')
.where('id', '=', id)
.returningAll()
.execute();
return feedback;
} catch (error) {
console.error('Error deleting feedback:', error);
throw error;
}
}

export async function getFeedbackStats() {
try {
const stats = await mainDb
.selectFrom('feedback')
.select([
sql<number>`COUNT(*)`.as('total_feedback'),
sql<number>`AVG(rating)`.as('average_rating'),
sql<number>`COUNT(CASE WHEN rating = 5 THEN 1 END)`.as('five_star'),
sql<number>`COUNT(CASE WHEN rating = 4 THEN 1 END)`.as('four_star'),
sql<number>`COUNT(CASE WHEN rating = 3 THEN 1 END)`.as('three_star'),
sql<number>`COUNT(CASE WHEN rating = 2 THEN 1 END)`.as('two_star'),
sql<number>`COUNT(CASE WHEN rating = 1 THEN 1 END)`.as('one_star'),
])
.executeTakeFirst();
return stats;
} catch (error) {
console.error('Error fetching feedback stats:', error);
throw error;
}
}
13 changes: 13 additions & 0 deletions server/db/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ export async function createMainTables() {
.addColumn('ip_address', 'varchar(255)')
.addColumn('timestamp', 'timestamp', (col) => col.defaultTo('now()'))
.execute();

// feedback
await mainDb.schema
.createTable('feedback')
.ifNotExists()
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('user_id', 'varchar(255)', (col) => col.notNull())
.addColumn('username', 'varchar(255)', (col) => col.notNull())
.addColumn('rating', 'integer', (col) => col.notNull())
.addColumn('comment', 'text')
.addColumn('created_at', 'timestamp', (col) => col.defaultTo('now()'))
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo('now()'))
.execute();
}

// Helper to create a dynamic flights table for a session
Expand Down
2 changes: 2 additions & 0 deletions server/db/types/connection/MainDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ChatReportsTable } from "./main/ChatReportsTable";
import { UpdateModalsTable } from "./main/UpdateModalsTable";
import { FlightLogsTable } from "./main/FlightLogsTable";
import { GlobalHolidaySettingsTable } from "./main/GlobalHolidaySettingsTable";
import { FeedbackTable } from "./main/FeedbackTable";

export interface MainDatabase {
app_settings: AppSettingsTable;
Expand All @@ -32,4 +33,5 @@ export interface MainDatabase {
update_modals: UpdateModalsTable;
flight_logs: FlightLogsTable;
global_holiday_settings: GlobalHolidaySettingsTable;
feedback: FeedbackTable;
}
9 changes: 9 additions & 0 deletions server/db/types/connection/main/FeedbackTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface FeedbackTable {
id: number;
user_id: string;
username: string;
rating: number;
comment?: string;
created_at?: Date;
updated_at?: Date;
}
61 changes: 61 additions & 0 deletions server/routes/admin/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import express from 'express';
import { createAuditLogger } from '../../middleware/auditLogger.js';
import { logAdminAction } from '../../db/audit.js';
import {
getAllFeedback,
deleteFeedback,
getFeedbackStats
} from '../../db/feedback.js';
import { getClientIp } from '../../utils/getIpAddress.js';

const router = express.Router();

// GET: /api/admin/feedback - Get all feedback
router.get('/', createAuditLogger('ADMIN_FEEDBACK_ACCESSED'), async (req, res) => {
try {
const feedback = await getAllFeedback();
res.json(feedback);
} catch (error) {
console.error('Error fetching feedback:', error);
res.status(500).json({ error: 'Failed to fetch feedback' });
}
});

// GET: /api/admin/feedback/stats - Get feedback statistics
router.get('/stats', async (req, res) => {
try {
const stats = await getFeedbackStats();
res.json(stats);
} catch (error) {
console.error('Error fetching feedback stats:', error);
res.status(500).json({ error: 'Failed to fetch feedback stats' });
}
});

// DELETE: /api/admin/feedback/:id - Delete feedback
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const numericId = Number(id);

const feedback = await deleteFeedback(numericId);

if (req.user?.userId) {
const ip = getClientIp(req);
await logAdminAction({
adminId: req.user.userId,
adminUsername: req.user.username || 'Unknown',
actionType: 'FEEDBACK_DELETED',
ipAddress: Array.isArray(ip) ? ip.join(', ') : ip,
details: { message: `Deleted feedback with ID: ${numericId}` },
});
}

res.json(feedback);
} catch (error) {
console.error('Error deleting feedback:', error);
res.status(500).json({ error: 'Failed to delete feedback' });
}
});

export default router;
2 changes: 2 additions & 0 deletions server/routes/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import rolesRouter from './roles.js';
import chatReportsRouter from './chat-reports.js';
import updateModalsRouter from './updateModals.js';
import flightLogsRouter from './flight-logs.js';
import feedbackRouter from './feedback.js';

const router = express.Router();

Expand All @@ -36,6 +37,7 @@ router.use('/roles', rolesRouter);
router.use('/chat-reports', chatReportsRouter);
router.use('/update-modals', updateModalsRouter);
router.use('/flight-logs', flightLogsRouter);
router.use('/feedback', feedbackRouter);

// GET: /api/admin/statistics - Get dashboard statistics
router.get('/statistics', requirePermission('admin'), async (req, res) => {
Expand Down
7 changes: 5 additions & 2 deletions server/routes/admin/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ router.use(requirePermission('sessions'));
// GET: /api/admin/sessions - Get all sessions with details
router.get('/', createAuditLogger('ADMIN_SESSIONS_ACCESSED'), async (req, res) => {
try {
const sessions = await getAdminSessions();
res.json(sessions);
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 100, 100);
const search = (req.query.search as string) || '';
const result = await getAdminSessions(page, limit, search);
res.json(result);
} catch (error) {
console.error('Error fetching sessions:', error);
res.status(500).json({ error: 'Failed to fetch sessions' });
Expand Down
30 changes: 30 additions & 0 deletions server/routes/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import express from 'express';
import { addFeedback } from '../db/feedback.js';
import requireAuth from '../middleware/auth.js';

const router = express.Router();

// POST: /api/feedback - Submit feedback
router.post('/', requireAuth, async (req, res) => {
try {
const { rating, comment } = req.body;

if (!rating || rating < 1 || rating > 5) {
return res.status(400).json({ error: 'Rating must be between 1 and 5' });
}

const feedback = await addFeedback({
userId: req.user!.userId || '',
username: req.user!.username || '',
rating: Number(rating),
comment: comment?.trim() || undefined,
});

res.json(feedback);
} catch (error) {
console.error('Error submitting feedback:', error);
res.status(500).json({ error: 'Failed to submit feedback' });
}
});

export default router;
2 changes: 2 additions & 0 deletions server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import pilotRouter from './pilot.js';
import adminRouter from './admin/index.js';
import updateModalRouter from './updateModal.js';
import versionRouter from './version.js';
import feedbackRouter from './feedback.js';

const router = express.Router();

Expand All @@ -27,5 +28,6 @@ router.use('/pilot', pilotRouter);
router.use('/admin', adminRouter);
router.use('/update-modal', updateModalRouter);
router.use('/version', versionRouter);
router.use('/feedback', feedbackRouter);

export default router;
9 changes: 9 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import AdminNotifications from './pages/admin/AdminNotifications';
import AdminRoles from './pages/admin/AdminRoles';
import AdminChatReports from './pages/admin/AdminChatReports';
import AdminFlightLogs from './pages/admin/AdminFlightLogs';
import AdminFeedback from './pages/admin/AdminFeedback';
import { getTesterSettings } from './utils/fetch/data';

import {
Expand Down Expand Up @@ -305,6 +306,14 @@ export default function App() {
</ProtectedRoute>
}
/>
<Route
path="feedback"
element={
<ProtectedRoute requirePermission="admin">
<AdminFeedback />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</ProtectedRoute>
Expand Down
Loading
Loading