+
+
How can we help you?
+
Search our knowledge base for instant answers
+
+
+
+
+ {showSuggestions && }
+
-
- {CATEGORIES.map(cat => {
- const isActive = cat === activeCategory;
- return (
-
+ );
+ })}
+
- {showOverview && (
-
-
-
-
+ {showOverview && (
+
+
Programme Overview
+ setOverviewOpen(false)} style={{ padding: '6px 16px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', fontSize: 13, background: 'var(--bg-secondary)', color: 'var(--text-secondary)', cursor: 'pointer', fontFamily: 'inherit' }}>Back
+
+
+ {overviewLoading ?
Loadingβ¦
: overview ? overview.map((s, i) => sectionContent(s, i)) :
Failed to load overview
}
-
setOverviewOpen(false)} style={{
- padding: '6px 16px', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
- fontSize: 13, background: 'var(--bg-secondary)', color: 'var(--text-secondary)',
- cursor: 'pointer', fontFamily: 'inherit'
- }}>Back
-
- {overviewLoading ? (
-
Loadingβ¦
- ) : overview ? (
- overview.map((s, i) => sectionContent(s, i))
- ) : (
-
Failed to load overview
+ )}
+
+ {!showOverview && (faqsLoading ? (
+
+ ) : (
+
+ {!searchFaqIds && !activeCategory && topFaqs.length > 0 && (
+ <>
+
Trending FAQs
+
+ {topFaqs.map(faq => (
+
document.querySelector(`details[data-faq-id="${faq._id}"]`)?.scrollIntoView({ behavior: 'smooth' })}
+ style={{ padding: '10px 14px', background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)', cursor: 'pointer', fontSize: 14, color: 'var(--text-primary)', transition: 'border-color 150ms ease' }}
+ onMouseOver={e => e.currentTarget.style.borderColor = 'var(--accent)'}
+ onMouseOut={e => e.currentTarget.style.borderColor = 'var(--border)'}>
+ {faq.question}
+
+ ))}
+
+
All FAQs
+ >
)}
+ {activeCategory &&
Showing: {activeCategory.replace(/-/g, ' ')}
}
+ {displayedFaqs.length === 0 && !faqsLoading &&
{searchFaqIds ? 'No matching FAQs found.' : 'No FAQs in this category yet.'}
}
+ {displayedFaqs.map(faq => {
+ const matched = searchFaqIds?.includes(faq._id);
+ return (
+
{ if (el) faqRefs.current[faq._id] = el; }}
+ onToggle={e => { if (!e.target.open && highlightedFaq === faq._id) setHighlightedFaq(null); }}
+ style={{ display: searchFaqIds && !matched ? 'none' : 'block', background: matched ? 'var(--accent-light)' : 'var(--bg-card)', backdropFilter: 'blur(16px)', border: '1px solid var(--border)', borderRadius: 'var(--radius-md)', marginBottom: 8, overflow: 'hidden' }}>
+ {faq.question}
+ {faq.answer}
+
+ );
+ })}
-
- )}
+ ))}
+
- {!showOverview && (faqsLoading ? (
-
{[1, 2, 3, 4, 5].map(i => (
-
- ))}
- ) : (
-
- {!searchFaqIds && !activeCategory && topFaqs.length > 0 && (
- <>
-
Trending FAQs
-
- {topFaqs.map(faq => (
-
{
- document.querySelector(`details[data-faq-id="${faq._id}"]`)?.scrollIntoView({ behavior: 'smooth' });
- }} style={{
- padding: '10px 14px', background: 'var(--bg-secondary)',
- border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
- cursor: 'pointer', fontSize: 14, color: 'var(--text-primary)',
- transition: 'border-color 150ms ease'
- }}
- onMouseOver={e => e.currentTarget.style.borderColor = 'var(--accent)'}
- onMouseOut={e => e.currentTarget.style.borderColor = 'var(--border)'}>
- {faq.question}
-
- ))}
-
-
All FAQs
- >
- )}
- {activeCategory && (
-
- Showing: {activeCategory.replace(/-/g, ' ')}
-
- )}
- {displayedFaqs.length === 0 && !faqsLoading && (
-
- {searchFaqIds ? 'No matching FAQs found for your search.' : 'No FAQs in this category yet.'}
-
- )}
- {displayedFaqs.map(faq => {
- const matched = searchFaqIds?.includes(faq._id);
- return (
-
{ if (el) faqRefs.current[faq._id] = el; }}
- onToggle={e => { if (!e.target.open && highlightedFaq === faq._id) setHighlightedFaq(null); }}
- style={{
- display: searchFaqIds && !matched ? 'none' : 'block',
- background: matched ? 'var(--accent-light)' : 'var(--bg-card)',
- backdropFilter: 'blur(16px)', border: '1px solid var(--border)',
- borderRadius: 'var(--radius-md)', marginBottom: 8, overflow: 'hidden'
- }}>
-
- {faq.question}
-
-
- {faq.answer}
-
-
- );
- })}
-
- ))}
+ {user.role !== 'admin' && user.role !== 'super_admin' && (
+
navigate('/query')} title="Raise a Query" style={{ position: 'fixed', bottom: 100, right: 24, zIndex: 100, width: 56, height: 56, borderRadius: '50%', border: 'none', background: 'var(--accent)', color: '#fff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 4px 16px rgba(0,0,0,0.2)', transition: 'transform 150ms ease' }}
+ onMouseOver={e => e.currentTarget.style.transform = 'scale(1.1)'}
+ onMouseOut={e => e.currentTarget.style.transform = 'scale(1)'}>
+
+
+ )}
+
-
navigate('/query')} title="Raise a Query" style={{
- position: 'fixed', bottom: 100, right: 24, zIndex: 100,
- width: 56, height: 56, borderRadius: '50%', border: 'none',
- background: 'var(--accent)', color: '#fff', cursor: 'pointer',
- display: 'flex', alignItems: 'center', justifyContent: 'center',
- boxShadow: '0 4px 16px rgba(0,0,0,0.2)', transition: 'transform 150ms ease',
- fontSize: 24, fontFamily: 'inherit'
- }}
- onMouseOver={e => e.currentTarget.style.transform = 'scale(1.1)'}
- onMouseOut={e => e.currentTarget.style.transform = 'scale(1)'}>
-
-
-
);
-}
+}
\ No newline at end of file
diff --git a/client/src/services/adminService.js b/client/src/services/adminService.js
new file mode 100644
index 0000000..f41249e
--- /dev/null
+++ b/client/src/services/adminService.js
@@ -0,0 +1,60 @@
+import api from '../api/axios';
+
+const adminService = {
+ getStats: async () => {
+ const { data } = await api.get('/admin/stats');
+ return data;
+ },
+
+ getUsers: async () => {
+ const { data } = await api.get('/admin/users');
+ return data;
+ },
+
+ promoteUser: async (id) => {
+ const { data } = await api.patch(`/admin/users/${id}/promote`);
+ return data;
+ },
+
+ promoteToSuperAdmin: async (id) => {
+ const { data } = await api.patch(`/admin/users/${id}/promote-super`);
+ return data;
+ },
+
+ demoteAdmin: async (id) => {
+ const { data } = await api.patch(`/admin/users/${id}/demote`);
+ return data;
+ },
+
+ deleteUser: async (id) => {
+ const { data } = await api.delete(`/admin/users/${id}`);
+ return data;
+ },
+
+ getAllFaqs: async () => {
+ const { data } = await api.get('/admin/faqs');
+ return data;
+ },
+
+ createFaq: async (payload) => {
+ const { data } = await api.post('/admin/faqs', payload);
+ return data;
+ },
+
+ updateFaq: async (id, payload) => {
+ const { data } = await api.patch(`/admin/faqs/${id}`, payload);
+ return data;
+ },
+
+ deleteFaq: async (id) => {
+ const { data } = await api.delete(`/admin/faqs/${id}`);
+ return data;
+ },
+
+ getAuditLogs: async () => {
+ const { data } = await api.get('/admin/audit-logs');
+ return data;
+ },
+};
+
+export default adminService;
\ No newline at end of file
diff --git a/client/src/services/answerService.js b/client/src/services/answerService.js
new file mode 100644
index 0000000..add3d63
--- /dev/null
+++ b/client/src/services/answerService.js
@@ -0,0 +1,47 @@
+import api from '../api/axios';
+
+// Create new answer
+const createAnswer = async (answerData) => {
+ const response = await api.post('/answers', answerData);
+ return response.data;
+};
+
+// Get all answers for a question
+const getAnswersByQuestionId = async (questionId) => {
+ const response = await api.get(`/answers/${questionId}`);
+ return response.data;
+};
+
+// Delete answer (Admin)
+const deleteAnswer = async (id) => {
+ const response = await api.delete(`/answers/${id}`);
+ return response.data;
+};
+
+const upvoteAnswer = async (id) => {
+ const response = await api.put(`/answers/${id}/upvote`);
+ return response.data;
+};
+
+const downvoteAnswer = async (id) => {
+ const response = await api.put(`/answers/${id}/downvote`);
+ return response.data;
+};
+
+const acceptAnswer = async (id) => {
+ const response = await api.put(`/answers/${id}/accept`);
+ return response.data;
+};
+
+
+
+const answerService = {
+ createAnswer,
+ getAnswersByQuestionId,
+ deleteAnswer,
+ upvoteAnswer,
+ downvoteAnswer,
+ acceptAnswer
+};
+
+export default answerService;
diff --git a/client/src/services/authService.js b/client/src/services/authService.js
new file mode 100644
index 0000000..4c1faf2
--- /dev/null
+++ b/client/src/services/authService.js
@@ -0,0 +1,20 @@
+import api from '../api/axios';
+
+// Register user
+const register = async (username, email, password, isAdmin) => {
+ const response = await api.post('/auth/register', { username, email, password, isAdmin });
+ return response.data;
+};
+
+// Login user
+const login = async (userData) => {
+ const response = await api.post('/auth/login', userData);
+ return response.data;
+};
+
+const authService = {
+ register,
+ login,
+};
+
+export default authService;
diff --git a/client/src/services/faqService.js b/client/src/services/faqService.js
new file mode 100644
index 0000000..f933a1d
--- /dev/null
+++ b/client/src/services/faqService.js
@@ -0,0 +1,41 @@
+import api from '../api/axios';
+
+// Create new FAQ (Admin only)
+const createFaq = async (faqData) => {
+ const response = await api.post('/faqs', faqData);
+ return response.data;
+};
+
+// Get all FAQs
+const getFaqs = async () => {
+ const response = await api.get('/faqs');
+ return response.data;
+};
+
+// Get single FAQ
+const getFaqById = async (id) => {
+ const response = await api.get(`/faqs/${id}`);
+ return response.data;
+};
+
+// Update FAQ (Admin only)
+const updateFaq = async (id, faqData) => {
+ const response = await api.put(`/faqs/${id}`, faqData);
+ return response.data;
+};
+
+// Delete FAQ (Admin only)
+const deleteFaq = async (id) => {
+ const response = await api.delete(`/faqs/${id}`);
+ return response.data;
+};
+
+const faqService = {
+ createFaq,
+ getFaqs,
+ getFaqById,
+ updateFaq,
+ deleteFaq
+};
+
+export default faqService;
diff --git a/client/src/services/questionService.js b/client/src/services/questionService.js
new file mode 100644
index 0000000..445dec9
--- /dev/null
+++ b/client/src/services/questionService.js
@@ -0,0 +1,41 @@
+import api from '../api/axios';
+
+// Create new question
+const createQuestion = async (questionData) => {
+ const response = await api.post('/questions', questionData);
+ return response.data;
+};
+
+// Get all questions
+const getQuestions = async () => {
+ const response = await api.get('/questions');
+ return response.data;
+};
+
+// Get user's questions
+const getMyQuestions = async () => {
+ const response = await api.get('/questions/myquestions');
+ return response.data;
+};
+
+// Get single question by ID
+const getQuestionById = async (id) => {
+ const response = await api.get(`/questions/${id}`);
+ return response.data;
+};
+
+// Delete question (Admin)
+const deleteQuestion = async (id) => {
+ const response = await api.delete(`/questions/${id}`);
+ return response.data;
+};
+
+const questionService = {
+ createQuestion,
+ getQuestions,
+ getMyQuestions,
+ getQuestionById,
+ deleteQuestion
+};
+
+export default questionService;
diff --git a/client/src/styles/auth.css b/client/src/styles/auth.css
index 09337e5..5c827d1 100644
--- a/client/src/styles/auth.css
+++ b/client/src/styles/auth.css
@@ -659,3 +659,160 @@ a { color: inherit; text-decoration: none; }
font-size: 14px;
}
}
+
+/* ===== Shared Component Classes ===== */
+.glass-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 24px;
+ margin-bottom: 16px;
+ backdrop-filter: blur(8px);
+}
+
+.flex-between {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.flex-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.flex-col {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.form-input {
+ width: 100%;
+ padding: 10px 14px;
+ border: 1.5px solid var(--border);
+ border-radius: var(--radius-sm);
+ font-size: 14px;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-family: var(--font);
+ transition: border-color var(--transition);
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: var(--border-focus);
+}
+
+.badge {
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.badge-answered {
+ background: var(--success-bg);
+ color: var(--success);
+}
+
+.badge-pending {
+ background: rgba(217, 119, 6, 0.1);
+ color: var(--warning);
+}
+
+.alert-error {
+ padding: 12px 16px;
+ background: var(--error-bg);
+ color: var(--error);
+ border-radius: var(--radius-sm);
+ font-size: 14px;
+}
+
+/* ===== Sidebar Layout ===== */
+.app-layout {
+ display: flex;
+ min-height: 100vh;
+}
+
+.sidebar {
+ width: 260px;
+ background: var(--bg-card);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ z-index: 50;
+}
+
+.sidebar-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid var(--border);
+ font-weight: 700;
+ color: var(--accent);
+}
+
+.nav-links {
+ flex: 1;
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.nav-item {
+ display: flex;
+ align-items: center;
+ padding: 10px 14px;
+ border-radius: var(--radius-sm);
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: all var(--transition);
+}
+
+.nav-item:hover {
+ background: var(--accent-light);
+ color: var(--accent);
+}
+
+.nav-item.active {
+ background: var(--accent-light);
+ color: var(--accent);
+ font-weight: 600;
+}
+
+.sidebar-footer {
+ padding: 16px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.main-content {
+ flex: 1;
+ margin-left: 260px;
+ display: flex;
+ flex-direction: column;
+}
+
+.top-header {
+ padding: 16px 24px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-card);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.page-container {
+ padding: 32px;
+ flex: 1;
+}
diff --git a/client/src/styles/chatbot.css b/client/src/styles/chatbot.css
index ac4d08c..ebd5da2 100644
--- a/client/src/styles/chatbot.css
+++ b/client/src/styles/chatbot.css
@@ -519,3 +519,15 @@
.neural-faq-sidebar { grid-template-columns: 1fr; }
.chat-message-content { max-width: 90%; }
}
+
+.chat-message-text {
+ word-break: break-word;
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+ max-width: 100%;
+}
+
+.chat-message-content {
+ min-width: 0;
+ max-width: 100%;
+}
\ No newline at end of file
diff --git a/client/src/styles/yaksha.css b/client/src/styles/yaksha.css
index 3e43ff0..11c94f0 100644
--- a/client/src/styles/yaksha.css
+++ b/client/src/styles/yaksha.css
@@ -278,3 +278,23 @@
border-radius: 14px 14px 0 0;
}
}
+.ym-bubble {
+ word-break: break-word;
+ overflow-wrap: break-word;
+ white-space: pre-wrap;
+ max-width: 100%;
+ min-width: 0;
+}
+
+.ym-source-item {
+ word-break: break-word;
+ overflow-wrap: break-word;
+}
+
+.yaksha-mini {
+ overflow: hidden;
+}
+
+.yaksha-mini-log {
+ overflow-x: hidden;
+}
diff --git a/server/controllers/adminController.js b/server/controllers/adminController.js
index 49eaad8..c82a25e 100644
--- a/server/controllers/adminController.js
+++ b/server/controllers/adminController.js
@@ -1,22 +1,29 @@
const User = require('../models/User');
+const Answer = require('../models/Answer');
+const Question = require('../models/Question');
+const Faq = require('../models/Faq');
+const AuditLog = require('../models/AuditLog');
const { AppError } = require('../middleware/errorHandler');
+const log = async (action, performedBy, details = {}) => {
+ try {
+ await AuditLog.create({ action, performedBy, ...details });
+ } catch (e) {
+ console.warn('Audit log failed:', e.message);
+ }
+};
+
exports.getUsers = async (req, res, next) => {
try {
const { page = 1, limit = 20, role, search } = req.query;
const query = {};
-
- if (role && ['super_admin', 'admin', 'intern'].includes(role)) {
- query.role = role;
- }
-
+ if (role && ['super_admin', 'admin', 'intern'].includes(role)) query.role = role;
if (search) {
query.$or = [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } },
];
}
-
const skip = (parseInt(page) - 1) * parseInt(limit);
const [users, total] = await Promise.all([
User.find(query)
@@ -26,120 +33,239 @@ exports.getUsers = async (req, res, next) => {
.limit(parseInt(limit)),
User.countDocuments(query),
]);
-
- res.json({
- success: true,
- users,
- pagination: {
- page: parseInt(page),
- limit: parseInt(limit),
- total,
- pages: Math.ceil(total / parseInt(limit)),
- },
- });
- } catch (err) {
- next(err);
- }
+ res.json({ success: true, users, pagination: { page: parseInt(page), limit: parseInt(limit), total, pages: Math.ceil(total / parseInt(limit)) } });
+ } catch (err) { next(err); }
};
exports.getUserById = async (req, res, next) => {
try {
- const user = await User.findById(req.params.id).select('-refreshToken -passwordResetToken -passwordResetExpires');
- if (!user) {
- return next(new AppError('User not found', 404));
- }
+ const user = await User.findById(req.params.id)
+ .select('-refreshToken -passwordResetToken -passwordResetExpires');
+ if (!user) return next(new AppError('User not found', 404));
res.json({ success: true, user });
- } catch (err) {
- next(err);
- }
+ } catch (err) { next(err); }
};
exports.updateUserRole = async (req, res, next) => {
try {
const { role } = req.body;
- if (!role || !['super_admin', 'admin', 'intern'].includes(role)) {
- return next(new AppError('Valid role is required (super_admin, admin, intern)', 400));
- }
+ if (!role || !['super_admin', 'admin', 'intern'].includes(role))
+ return next(new AppError('Valid role required', 400));
const user = await User.findById(req.params.id);
- if (!user) {
- return next(new AppError('User not found', 404));
- }
-
- if (user.role === 'super_admin' && req.user.role !== 'super_admin') {
- return next(new AppError('Only super admins can modify super admin accounts', 403));
- }
-
- if (role === 'super_admin' && req.user.role !== 'super_admin') {
- return next(new AppError('Only super admins can assign super admin role', 403));
- }
+ if (!user) return next(new AppError('User not found', 404));
+ if (user._id.equals(req.user._id)) return next(new AppError('Cannot change your own role', 400));
+ if (user.role === 'super_admin') return next(new AppError('Cannot change a super_admin role', 403));
+ if (role === 'super_admin' && req.user.role !== 'super_admin')
+ return next(new AppError('Only super_admin can assign super_admin', 403));
+ const oldRole = user.role;
user.role = role;
await user.save();
+ await log('role_change', req.user._id, { targetUser: user._id, details: `${oldRole} β ${role}` });
- res.json({
- success: true,
- message: `User role updated to ${role}`,
- user: {
- id: user._id,
- name: user.name,
- email: user.email,
- role: user.role,
- },
- });
- } catch (err) {
- next(err);
- }
+ res.json({ success: true, message: `Role updated to ${role}`, user: { id: user._id, name: user.name, email: user.email, role: user.role } });
+ } catch (err) { next(err); }
};
exports.promoteToAdmin = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
- if (!user) {
- return next(new AppError('User not found', 404));
- }
-
- if (user.role !== 'intern') {
- return next(new AppError('Only interns can be promoted to admin', 400));
- }
+ if (!user) return next(new AppError('User not found', 404));
+ if (user.role === 'admin') return next(new AppError('Already an admin', 400));
+ if (user.role === 'super_admin') return next(new AppError('Cannot demote super_admin', 403));
+ if (user._id.equals(req.user._id)) return next(new AppError('Cannot promote yourself', 400));
user.role = 'admin';
await user.save();
+ await log('promote_to_admin', req.user._id, { targetUser: user._id, details: `${user.name} promoted to admin` });
- res.json({
- success: true,
- message: 'Intern promoted to admin successfully',
- user: {
- id: user._id,
- name: user.name,
- email: user.email,
- role: user.role,
- },
- });
- } catch (err) {
- next(err);
- }
+ res.json({ success: true, message: 'Promoted to admin', user: { id: user._id, name: user.name, email: user.email, role: user.role } });
+ } catch (err) { next(err); }
};
-exports.deleteUser = async (req, res, next) => {
+exports.promoteToSuperAdmin = async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
- if (!user) {
- return next(new AppError('User not found', 404));
- }
+ if (!user) return next(new AppError('User not found', 404));
+ if (user.role === 'super_admin') return next(new AppError('Already a super_admin', 400));
+ if (user._id.equals(req.user._id)) return next(new AppError('Cannot promote yourself', 400));
- if (user.role === 'super_admin') {
- return next(new AppError('Cannot delete a super admin account', 403));
- }
+ user.role = 'super_admin';
+ await user.save();
+ await log('promote_to_super_admin', req.user._id, { targetUser: user._id, details: `${user.name} promoted to super_admin` });
- if (user._id.equals(req.user._id)) {
- return next(new AppError('Cannot delete your own account', 400));
- }
+ res.json({ success: true, message: 'Promoted to super_admin', user: { id: user._id, name: user.name, email: user.email, role: user.role } });
+ } catch (err) { next(err); }
+};
+
+exports.demoteAdmin = async (req, res, next) => {
+ try {
+ const user = await User.findById(req.params.id);
+ if (!user) return next(new AppError('User not found', 404));
+ if (user.role === 'super_admin') return next(new AppError('Cannot demote a super_admin', 403));
+ if (user.role === 'intern') return next(new AppError('User is already an intern', 400));
+ if (user._id.equals(req.user._id)) return next(new AppError('Cannot demote yourself', 400));
+
+ user.role = 'intern';
+ await user.save();
+ await log('demote_to_intern', req.user._id, { targetUser: user._id, details: `${user.name} demoted to intern` });
+
+ res.json({ success: true, message: 'Demoted to intern', user: { id: user._id, name: user.name, email: user.email, role: user.role } });
+ } catch (err) { next(err); }
+};
+exports.deleteUser = async (req, res, next) => {
+ try {
+ const user = await User.findById(req.params.id);
+ if (!user) return next(new AppError('User not found', 404));
+ if (user._id.equals(req.user._id)) return next(new AppError('Cannot delete your own account', 400));
+ if (user.role === 'super_admin') return next(new AppError('Cannot delete a super_admin', 403));
+ if (user.role === 'admin' && req.user.role !== 'super_admin')
+ return next(new AppError('Only super_admin can delete admins', 403));
+
+ await log('delete_user', req.user._id, { targetUser: user._id, details: `Deleted ${user.name} (${user.role})` });
await User.findByIdAndDelete(req.params.id);
+ res.json({ success: true, message: 'User deleted' });
+ } catch (err) { next(err); }
+};
- res.json({ success: true, message: 'User deleted successfully' });
- } catch (err) {
- next(err);
- }
+exports.getStats = async (req, res, next) => {
+ try {
+ const Question = require('../models/Question');
+ const Answer = require('../models/Answer');
+ const Faq = require('../models/Faq');
+ const [totalUsers, totalQuestions, totalAnswers, totalFaqs, pendingAnswers] = await Promise.all([
+ User.countDocuments(),
+ Question.countDocuments(),
+ Answer.countDocuments(),
+ Faq.countDocuments(),
+ Answer.countDocuments({ status: 'pending' }),
+ ]);
+ res.json({ success: true, totalUsers, totalQuestions, totalAnswers, totalFaqs, pendingAnswers });
+ } catch (err) { next(err); }
+};
+
+exports.markGoodQuestion = async (req, res, next) => {
+ try {
+ const Question = require('../models/Question');
+ const question = await Question.findById(req.params.id);
+ if (!question) return next(new AppError('Question not found', 404));
+ question.isGoodQuestion = true;
+ await question.save();
+ try {
+ const author = await User.findById(question.author);
+ if (author) await author.awardPoints('good_question', 5, question._id);
+ } catch (e) { console.warn('Points award failed:', e.message); }
+ res.json({ success: true, message: 'Marked as Good Question', question });
+ } catch (err) { next(err); }
+};
+
+exports.promoteQuestionToFaq = async (req, res, next) => {
+ try {
+ const Question = require('../models/Question');
+ const Faq = require('../models/Faq');
+ const { answer, category } = req.body;
+ if (!answer || !category) return next(new AppError('Answer and category required', 400));
+ const question = await Question.findById(req.params.id);
+ if (!question) return next(new AppError('Question not found', 404));
+ const faq = await Faq.create({ question: question.title, answer, category: category.toLowerCase(), createdBy: req.user._id });
+ try { const { indexFaq } = require('../services/searchService'); await indexFaq(faq); } catch (e) { console.warn('Index warning:', e.message); }
+ try {
+ const author = await User.findById(question.author);
+ if (author) await author.awardPoints('question_promoted_to_faq', 15, question._id);
+ } catch (e) { console.warn('Points award failed:', e.message); }
+ await log('promote_question_to_faq', req.user._id, { targetFaq: faq._id, details: question.title });
+ res.json({ success: true, message: 'Promoted to FAQ', faq });
+ } catch (err) { next(err); }
+};
+
+// FAQ CRUD (super_admin only)
+exports.getAllFaqs = async (req, res, next) => {
+ try {
+ const Faq = require('../models/Faq');
+ const faqs = await Faq.find({}).sort({ createdAt: -1 });
+ res.json({ success: true, faqs });
+ } catch (err) { next(err); }
+};
+
+exports.createFaq = async (req, res, next) => {
+ try {
+ const Faq = require('../models/Faq');
+ const { question, answer, category } = req.body;
+ if (!question || !answer || !category) return next(new AppError('question, answer, category required', 400));
+ const faq = await Faq.create({ question, answer, category: category.toLowerCase(), createdBy: req.user._id });
+ try { const { indexFaq } = require('../services/searchService'); await indexFaq(faq); } catch (e) {}
+ await log('create_faq', req.user._id, { targetFaq: faq._id, details: question });
+ res.json({ success: true, faq });
+ } catch (err) { next(err); }
+};
+
+exports.updateFaq = async (req, res, next) => {
+ try {
+ const Faq = require('../models/Faq');
+ const { question, answer, category } = req.body;
+ const faq = await Faq.findByIdAndUpdate(
+ req.params.id,
+ { question, answer, category: category?.toLowerCase() },
+ { new: true, runValidators: true }
+ );
+ if (!faq) return next(new AppError('FAQ not found', 404));
+ try { const { indexFaq } = require('../services/searchService'); await indexFaq(faq); } catch (e) {}
+ await log('update_faq', req.user._id, { targetFaq: faq._id, details: question });
+ res.json({ success: true, faq });
+ } catch (err) { next(err); }
+};
+
+exports.deleteFaq = async (req, res, next) => {
+ try {
+ const Faq = require('../models/Faq');
+ const faq = await Faq.findByIdAndDelete(req.params.id);
+ if (!faq) return next(new AppError('FAQ not found', 404));
+ try { const { deleteFaqIndex } = require('../services/searchService'); await deleteFaqIndex(req.params.id); } catch (e) {}
+ await log('delete_faq', req.user._id, { details: faq.question });
+ res.json({ success: true, message: 'FAQ deleted' });
+ } catch (err) { next(err); }
+};
+
+exports.getAuditLogs = async (req, res, next) => {
+ try {
+ const logs = await AuditLog.find({})
+ .populate('performedBy', 'name email role')
+ .populate('targetUser', 'name email role')
+ .sort({ createdAt: -1 })
+ .limit(100);
+ res.json({ success: true, logs });
+ } catch (err) { next(err); }
};
+
+exports.getPendingAnswers = async (req, res, next) => {
+ try {
+ const page = parseInt(req.query.page) || 1;
+ const limit = parseInt(req.query.limit) || 20;
+ const skip = (page - 1) * limit;
+ const [answers, total] = await Promise.all([
+ Answer.find({ status: 'pending' })
+ .populate('author', 'name email')
+ .populate('question', 'title')
+ .sort({ createdAt: -1 })
+ .skip(skip).limit(limit),
+ Answer.countDocuments({ status: 'pending' }),
+ ]);
+ res.json({
+ success: true,
+ answers,
+ pagination: { page, limit, total, pages: Math.ceil(total / limit) },
+ });
+ } catch (err) { next(err); }
+};
+
+exports.getLeaderboard = async (req, res, next) => {
+ try {
+ const users = await User.find({ role: 'intern', points: { $gt: 0 } })
+ .select('name email points role')
+ .sort({ points: -1 })
+ .limit(20);
+ res.json({ success: true, leaderboard: users });
+ } catch (err) { next(err); }
+};
\ No newline at end of file
diff --git a/server/controllers/answerController.js b/server/controllers/answerController.js
new file mode 100644
index 0000000..5a36584
--- /dev/null
+++ b/server/controllers/answerController.js
@@ -0,0 +1,283 @@
+const Answer = require('../models/Answer');
+const Question = require('../models/Question');
+const User = require('../models/User');
+const Notification = require('../models/Notification');
+const { notifyUser } = require('../services/socketService');
+
+// Create a new answer
+const createAnswer = async (req, res) => {
+ const { content, questionId } = req.body;
+
+ if (!content) {
+ return res.status(400).json({ message: 'Answer content is required' });
+ }
+
+ try {
+ const isAdmin = ['admin', 'super_admin'].includes(req.user.role);
+ const answer = await Answer.create({
+ content,
+ question: questionId,
+ author: req.user._id,
+ status: isAdmin ? 'approved' : 'pending',
+ });
+
+ await Question.findByIdAndUpdate(questionId, { status: 'answered' });
+
+ const question = await Question.findById(questionId).select('author title');
+ if (question && question.author.toString() !== req.user._id.toString()) {
+ const notif = await Notification.create({
+ recipient: question.author,
+ type: 'question_answered',
+ title: 'Your Question Got an Answer',
+ message: `"${question.title.slice(0, 60)}" received a new answer`,
+ link: `/questions/${questionId}`,
+ relatedId: questionId,
+ });
+ notifyUser(question.author, notif);
+ }
+
+ res.status(201).json(answer);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Get all answers for a specific question
+const getAnswersByQuestionId = async (req, res) => {
+ try {
+ const isAdmin = ['admin', 'super_admin'].includes(req.user.role);
+ const filter = { question: req.params.questionId };
+ if (!isAdmin) {
+ filter.$or = [
+ { status: 'approved' },
+ { author: req.user._id, status: 'pending' },
+ ];
+ }
+ const answers = await Answer.find(filter)
+ .populate('author', 'name email points role')
+ .sort({ createdAt: 1 });
+ res.json(answers);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Delete answer (Admin only)
+const deleteAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findById(req.params.id);
+ if (!answer) {
+ return res.status(404).json({ message: 'Answer not found' });
+ }
+ await answer.deleteOne();
+ res.json({ message: 'Answer removed' });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Upvote an answer
+const upvoteAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findById(req.params.id);
+ if (!answer) return res.status(404).json({ message: 'Answer not found' });
+
+ const alreadyUpvoted = answer.upvotes.includes(req.user._id);
+
+ if (alreadyUpvoted) {
+ // Remove upvote (toggle off)
+ answer.upvotes = answer.upvotes.filter(id => id.toString() !== req.user._id.toString());
+ } else {
+ // Add upvote, remove downvote if exists
+ answer.upvotes.push(req.user._id);
+ const hadDownvote = answer.downvotes.includes(req.user._id);
+ answer.downvotes = answer.downvotes.filter(id => id.toString() !== req.user._id.toString());
+
+ // Award +5 points to answer author when they reach 5+ upvotes
+ if (answer.upvotes.length === 5) {
+ try {
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('answer_5_upvotes', 10, answer._id);
+ }
+ } catch (e) {
+ console.warn('Points award failed:', e.message);
+ }
+ }
+
+ // If removing a downvote, reverse the -5 penalty
+ if (hadDownvote) {
+ try {
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('downvote_removed', 5, answer._id);
+ }
+ } catch (e) {
+ console.warn('Points reversal failed:', e.message);
+ }
+ }
+ }
+
+ await answer.save();
+ res.json(answer);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Downvote an answer
+const downvoteAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findById(req.params.id);
+ if (!answer) return res.status(404).json({ message: 'Answer not found' });
+
+ const alreadyDownvoted = answer.downvotes.includes(req.user._id);
+
+ if (alreadyDownvoted) {
+ // Remove downvote (toggle off) β reverse the penalty
+ answer.downvotes = answer.downvotes.filter(id => id.toString() !== req.user._id.toString());
+ try {
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('downvote_removed', 5, answer._id);
+ }
+ } catch (e) {
+ console.warn('Points reversal failed:', e.message);
+ }
+ } else {
+ // Add downvote, remove upvote if exists
+ answer.downvotes.push(req.user._id);
+ answer.upvotes = answer.upvotes.filter(id => id.toString() !== req.user._id.toString());
+
+ // Award -5 points to answer author
+ try {
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('answer_downvoted', -5, answer._id);
+ }
+ } catch (e) {
+ console.warn('Points award failed:', e.message);
+ }
+ }
+
+ await answer.save();
+ res.json(answer);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Accept answer (Admin only) β +20 points
+const acceptAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findById(req.params.id);
+ if (!answer) return res.status(404).json({ message: 'Answer not found' });
+
+ answer.isAccepted = true;
+ await answer.save();
+
+ // Award +20 points to answer author
+ try {
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('answer_accepted', 20, answer._id);
+ }
+ } catch (e) {
+ console.warn('Points award failed:', e.message);
+ }
+
+ res.json(answer);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Approve answer (Admin only)
+const approveAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findByIdAndUpdate(
+ req.params.id,
+ { status: 'approved', reviewedBy: req.user._id, reviewedAt: new Date() },
+ { new: true }
+ );
+ if (!answer) return res.status(404).json({ message: 'Answer not found' });
+
+ const question = await Question.findById(answer.question).select('author title');
+ if (question) {
+ if (question.author.toString() !== answer.author.toString()) {
+ const notif = await Notification.create({
+ recipient: question.author,
+ type: 'answer_approved',
+ title: 'Answer Approved on Your Question',
+ message: `An answer was approved on "${question.title.slice(0, 60)}"`,
+ link: `/questions/${answer.question}`,
+ relatedId: answer.question,
+ });
+ notifyUser(question.author, notif);
+ }
+ const notif = await Notification.create({
+ recipient: answer.author,
+ type: 'answer_approved',
+ title: 'Your Answer Was Approved',
+ message: `"${question.title.slice(0, 60)}" answer was approved`,
+ link: `/questions/${answer.question}`,
+ relatedId: answer.question,
+ });
+ notifyUser(answer.author, notif);
+ }
+
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('answer_approved', 20, answer._id);
+ }
+
+ res.json({ success: true, answer });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Reject answer (Admin only)
+const rejectAnswer = async (req, res) => {
+ try {
+ const answer = await Answer.findByIdAndUpdate(
+ req.params.id,
+ { status: 'rejected', reviewedBy: req.user._id, reviewedAt: new Date() },
+ { new: true }
+ );
+ if (!answer) return res.status(404).json({ message: 'Answer not found' });
+
+ const author = await User.findById(answer.author);
+ if (author) {
+ await author.awardPoints('answer_rejected', -5, answer._id);
+ }
+
+ res.json({ success: true, answer });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+const getAllAnswers = async (req, res) => {
+ try {
+ const answers = await Answer.find({})
+ .populate('author', 'name email points role')
+ .populate('question', 'title')
+ .sort({ createdAt: -1 });
+ res.json(answers);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+module.exports = {
+ createAnswer,
+ getAnswersByQuestionId,
+ deleteAnswer,
+ upvoteAnswer,
+ downvoteAnswer,
+ acceptAnswer,
+ approveAnswer,
+ rejectAnswer,
+ getAllAnswers,
+};
\ No newline at end of file
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
index c3d8856..226cfa3 100644
--- a/server/controllers/authController.js
+++ b/server/controllers/authController.js
@@ -2,6 +2,8 @@ const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { OAuth2Client } = require('google-auth-library');
const User = require('../models/User');
+const Notification = require('../models/Notification');
+const { notifyUser } = require('../services/socketService');
const { AppError } = require('../middleware/errorHandler');
const googleClient = new OAuth2Client(
@@ -38,6 +40,7 @@ const sendTokens = (user, statusCode, res) => {
email: user.email,
role: user.role,
isVerified: user.isVerified,
+ points: user.points || 0,
},
});
};
@@ -59,6 +62,18 @@ exports.register = async (req, res, next) => {
isVerified: true,
});
+ const admins = await User.find({ role: { $in: ['admin', 'super_admin'] } }).select('_id');
+ for (const admin of admins) {
+ const notif = await Notification.create({
+ recipient: admin._id,
+ type: 'new_user',
+ title: 'New User Registered',
+ message: `${name.trim()} (${email.toLowerCase().trim()}) registered as an intern`,
+ link: '/admin?tab=users',
+ });
+ notifyUser(admin._id, notif);
+ }
+
sendTokens(user, 201, res);
} catch (err) {
next(err);
@@ -286,6 +301,7 @@ exports.getMe = async (req, res) => {
role: req.user.role,
isVerified: req.user.isVerified,
createdAt: req.user.createdAt,
+ points: req.user.points || 0,
},
});
};
diff --git a/server/controllers/faqController.js b/server/controllers/faqController.js
index 8cedba4..a54828d 100644
--- a/server/controllers/faqController.js
+++ b/server/controllers/faqController.js
@@ -1,20 +1,18 @@
ο»Ώconst Faq = require('../models/Faq');
const { AppError } = require('../middleware/errorHandler');
+const { indexFaq, deleteFaqIndex } = require('../services/searchService');
exports.getFAQs = async (req, res, next) => {
try {
const { category, page = 1, limit = 50 } = req.query;
const filter = { isPublished: true };
if (category) filter.category = category.toLowerCase();
-
const skip = (parseInt(page) - 1) * parseInt(limit);
const faqs = await Faq.find(filter)
.sort({ category: 1, views: -1 })
.skip(skip)
.limit(parseInt(limit));
-
const total = await Faq.countDocuments(filter);
-
res.json({ success: true, count: faqs.length, total, results: faqs });
} catch (err) {
next(err);
@@ -48,10 +46,8 @@ exports.searchFAQs = async (req, res, next) => {
try {
const { q } = req.query;
if (!q || !q.trim()) return next(new AppError('Search query is required', 400));
-
const query = q.trim();
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-
const results = await Faq.find({
isPublished: true,
$or: [
@@ -60,7 +56,6 @@ exports.searchFAQs = async (req, res, next) => {
{ category: { $regex: escaped, $options: 'i' } },
],
}).limit(10);
-
res.json({ success: true, count: results.length, results });
} catch (err) {
next(err);
@@ -91,6 +86,12 @@ exports.createFaq = async (req, res, next) => {
tags: tags || [],
createdBy: req.user?._id,
});
+ try {
+ await indexFaq(faq);
+ console.log(`Indexed new FAQ: "${faq.question}"`);
+ } catch (e) {
+ console.warn('FAQ index warning:', e.message);
+ }
res.status(201).json({ success: true, faq });
} catch (err) {
next(err);
@@ -99,8 +100,18 @@ exports.createFaq = async (req, res, next) => {
exports.updateFaq = async (req, res, next) => {
try {
- const faq = await Faq.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
+ const faq = await Faq.findByIdAndUpdate(
+ req.params.id,
+ req.body,
+ { new: true, runValidators: true }
+ );
if (!faq) return next(new AppError('FAQ not found', 404));
+ try {
+ await indexFaq(faq);
+ console.log(`Re-indexed updated FAQ: "${faq.question}"`);
+ } catch (e) {
+ console.warn('FAQ re-index warning:', e.message);
+ }
res.json({ success: true, faq });
} catch (err) {
next(err);
@@ -111,8 +122,14 @@ exports.deleteFaq = async (req, res, next) => {
try {
const faq = await Faq.findByIdAndDelete(req.params.id);
if (!faq) return next(new AppError('FAQ not found', 404));
+ try {
+ await deleteFaqIndex(req.params.id);
+ console.log(`Removed FAQ from index: "${faq.question}"`);
+ } catch (e) {
+ console.warn('FAQ delete index warning:', e.message);
+ }
res.json({ success: true, message: 'FAQ deleted' });
} catch (err) {
next(err);
}
-};
+};
\ No newline at end of file
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
new file mode 100644
index 0000000..9396805
--- /dev/null
+++ b/server/controllers/notificationController.js
@@ -0,0 +1,71 @@
+const Notification = require('../models/Notification');
+
+exports.getNotifications = async (req, res, next) => {
+ try {
+ const page = Math.max(1, parseInt(req.query.page) || 1);
+ const limit = Math.min(50, Math.max(1, parseInt(req.query.limit) || 20));
+ const skip = (page - 1) * limit;
+
+ const [notifications, total] = await Promise.all([
+ Notification.find({ recipient: req.user._id })
+ .sort({ createdAt: -1 })
+ .skip(skip)
+ .limit(limit)
+ .lean(),
+ Notification.countDocuments({ recipient: req.user._id }),
+ ]);
+
+ res.json({
+ success: true,
+ notifications,
+ pagination: {
+ page,
+ limit,
+ total,
+ pages: Math.ceil(total / limit),
+ },
+ });
+ } catch (err) {
+ next(err);
+ }
+};
+
+exports.getUnreadCount = async (req, res, next) => {
+ try {
+ const count = await Notification.countDocuments({
+ recipient: req.user._id,
+ read: false,
+ });
+ res.json({ success: true, count });
+ } catch (err) {
+ next(err);
+ }
+};
+
+exports.markAsRead = async (req, res, next) => {
+ try {
+ const notification = await Notification.findOneAndUpdate(
+ { _id: req.params.id, recipient: req.user._id },
+ { read: true },
+ { new: true }
+ );
+ if (!notification) {
+ return res.status(404).json({ success: false, message: 'Notification not found' });
+ }
+ res.json({ success: true, notification });
+ } catch (err) {
+ next(err);
+ }
+};
+
+exports.markAllAsRead = async (req, res, next) => {
+ try {
+ const result = await Notification.updateMany(
+ { recipient: req.user._id, read: false },
+ { read: true }
+ );
+ res.json({ success: true, modifiedCount: result.modifiedCount });
+ } catch (err) {
+ next(err);
+ }
+};
diff --git a/server/controllers/queryController.js b/server/controllers/queryController.js
index b40a72f..5a73e06 100644
--- a/server/controllers/queryController.js
+++ b/server/controllers/queryController.js
@@ -1,4 +1,7 @@
const Query = require('../models/Query');
+const Notification = require('../models/Notification');
+const User = require('../models/User');
+const { notifyUser, notifyAdmins } = require('../services/socketService');
const { AppError } = require('../middleware/errorHandler');
exports.createQuery = async (req, res, next) => {
@@ -15,6 +18,19 @@ exports.createQuery = async (req, res, next) => {
description: description || '',
});
+ const admins = await User.find({ role: { $in: ['admin', 'super_admin'] } }).select('_id');
+ for (const admin of admins) {
+ const notif = await Notification.create({
+ recipient: admin._id,
+ type: 'new_query',
+ title: 'New Query Submitted',
+ message: `${req.user.name || 'A user'} submitted a new query: "${query.question.slice(0, 80)}"`,
+ link: '/admin?tab=queries',
+ relatedId: query._id,
+ });
+ notifyUser(admin._id, notif);
+ }
+
res.status(201).json({ success: true, query });
} catch (err) {
next(err);
@@ -79,8 +95,34 @@ exports.respondToQuery = async (req, res, next) => {
}
await query.save();
+ const notif = await Notification.create({
+ recipient: query.user,
+ type: status === 'resolved' ? 'query_response' : 'query_status',
+ title: 'Query Update',
+ message: response
+ ? `Your query has been responded to: "${response.slice(0, 80)}"`
+ : `Your query status changed to "${status}"`,
+ link: '/query',
+ relatedId: query._id,
+ });
+ notifyUser(query.user, notif);
+
res.json({ success: true, query });
} catch (err) {
next(err);
}
};
+
+exports.deleteQuery = async (req, res, next) => {
+ try {
+ const query = await Query.findById(req.params.id);
+ if (!query) return next(new AppError('Query not found', 404));
+ if (query.user.toString() !== req.user._id.toString() && req.user.role !== 'admin' && req.user.role !== 'super_admin') {
+ return next(new AppError('Not authorized', 403));
+ }
+ await Query.findByIdAndDelete(req.params.id);
+ res.json({ success: true, message: 'Query deleted' });
+ } catch (err) {
+ next(err);
+ }
+};
\ No newline at end of file
diff --git a/server/controllers/questionController.js b/server/controllers/questionController.js
new file mode 100644
index 0000000..0736df0
--- /dev/null
+++ b/server/controllers/questionController.js
@@ -0,0 +1,93 @@
+const Question = require('../models/Question');
+const Notification = require('../models/Notification');
+const User = require('../models/User');
+const { notifyUser } = require('../services/socketService');
+
+// Create a new question
+const createQuestion = async (req, res) => {
+ const { title, description } = req.body;
+
+ if (!title) {
+ return res.status(400).json({ message: 'Title is required' });
+ }
+
+ try {
+ const question = await Question.create({
+ title,
+ description,
+ author: req.user._id
+ });
+
+ const admins = await User.find({ role: { $in: ['admin', 'super_admin'] } }).select('_id');
+ for (const admin of admins) {
+ const notif = await Notification.create({
+ recipient: admin._id,
+ type: 'new_question',
+ title: 'New Question Asked',
+ message: `${req.user.name || 'Someone'} asked: "${title.slice(0, 80)}"`,
+ link: '/admin?tab=questions',
+ relatedId: question._id,
+ });
+ notifyUser(admin._id, notif);
+ }
+
+ res.status(201).json(question);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Get all questions
+const getQuestions = async (req, res) => {
+ try {
+ const questions = await Question.find({}).populate('author', 'name email points').sort({ createdAt: -1 });
+ res.json(questions);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Get single question by ID
+const getQuestionById = async (req, res) => {
+ try {
+ const question = await Question.findById(req.params.id).populate('author', 'name email points');
+ if (!question) {
+ return res.status(404).json({ message: 'Question not found' });
+ }
+ res.json(question);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Get logged-in user's questions
+const getMyQuestions = async (req, res) => {
+ try {
+ const questions = await Question.find({ author: req.user._id }).sort({ createdAt: -1 });
+ res.json(questions);
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+// Delete question (Admin only)
+const deleteQuestion = async (req, res) => {
+ try {
+ const question = await Question.findById(req.params.id);
+ if (!question) {
+ return res.status(404).json({ message: 'Question not found' });
+ }
+ await question.deleteOne();
+ res.json({ message: 'Question removed' });
+ } catch (error) {
+ res.status(500).json({ message: error.message });
+ }
+};
+
+module.exports = {
+ createQuestion,
+ getQuestions,
+ getQuestionById,
+ getMyQuestions,
+ deleteQuestion
+};
diff --git a/server/controllers/searchController.js b/server/controllers/searchController.js
index d78ac0f..a5f2d2a 100644
--- a/server/controllers/searchController.js
+++ b/server/controllers/searchController.js
@@ -1,5 +1,10 @@
const { AppError } = require('../middleware/errorHandler');
-const { searchSimilar, getSuggestions, generateAnswer } = require('../services/searchService');
+const {
+ searchSimilar,
+ getSuggestions,
+ generateAnswer,
+ generateGeneralAnswer,
+} = require('../services/searchService');
const searchCache = new Map();
const CACHE_TTL = 5 * 60 * 1000;
@@ -26,7 +31,16 @@ exports.search = async (req, res, next) => {
}
const sources = await searchSimilar(trimmed, 5);
- const { answer, confidence } = await generateAnswer(trimmed, sources);
+const topScore = sources?.[0]?.score || 0;
+
+let answer, confidence;
+
+if (topScore < 0.25) {
+ ({ answer, confidence } = await generateGeneralAnswer(trimmed));
+} else {
+ ({ answer, confidence } = await generateAnswer(trimmed, sources));
+}
+ console.log(`Query: "${trimmed}" | Sources: ${sources.length} | Confidence: ${confidence}`);
const data = {
answer,
@@ -48,6 +62,35 @@ exports.search = async (req, res, next) => {
searchCache.set(trimmed, { data, timestamp: Date.now() });
+ // Auto-save unresolved FAQ queries only
+if (
+ req.user &&
+ topScore >= 0.25 &&
+ confidence < 0.4
+) {
+ try {
+ console.log('Attempting to save query for user:', req.user._id, 'Query:', trimmed);
+ const Query = require('../models/Query');
+ const existing = await Query.findOne({
+ question: { $regex: trimmed.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), $options: 'i' },
+ status: 'open'
+ });
+ if (!existing) {
+ await Query.create({
+ user: req.user._id,
+ question: trimmed,
+ category: 'general',
+ status: 'open'
+ });
+ console.log('Saved unresolved query:', trimmed);
+ } else {
+ console.log('Duplicate found, skipping');
+ }
+ } catch (e) {
+ console.log('Query save error:', e.message);
+ }
+ }
+
res.json({ success: true, ...data });
} catch (err) {
next(err);
@@ -75,4 +118,4 @@ exports.suggestions = async (req, res, next) => {
} catch (err) {
next(err);
}
-};
+};
\ No newline at end of file
diff --git a/server/models/Answer.js b/server/models/Answer.js
new file mode 100644
index 0000000..6fb9a67
--- /dev/null
+++ b/server/models/Answer.js
@@ -0,0 +1,42 @@
+const mongoose = require('mongoose');
+
+const answerSchema = new mongoose.Schema({
+ content: {
+ type: String,
+ required: true,
+ },
+ question: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: true,
+ ref: 'Question',
+ },
+ author: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: true,
+ ref: 'User',
+ },
+ upvotes: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ }],
+ downvotes: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ }],
+ isAccepted: {
+ type: Boolean,
+ default: false,
+ },
+ status: {
+ type: String,
+ enum: ['pending', 'approved', 'rejected'],
+ default: 'approved',
+ },
+ reviewedBy: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ },
+ reviewedAt: Date,
+}, { timestamps: true });
+
+module.exports = mongoose.model('Answer', answerSchema);
\ No newline at end of file
diff --git a/server/models/AuditLog.js b/server/models/AuditLog.js
new file mode 100644
index 0000000..d95a255
--- /dev/null
+++ b/server/models/AuditLog.js
@@ -0,0 +1,11 @@
+const mongoose = require('mongoose');
+
+const auditLogSchema = new mongoose.Schema({
+ action: { type: String, required: true },
+ performedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
+ targetUser: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ targetFaq: { type: mongoose.Schema.Types.ObjectId, ref: 'Faq' },
+ details: { type: String },
+}, { timestamps: true });
+
+module.exports = mongoose.model('AuditLog', auditLogSchema);
\ No newline at end of file
diff --git a/server/models/Notification.js b/server/models/Notification.js
new file mode 100644
index 0000000..76571c6
--- /dev/null
+++ b/server/models/Notification.js
@@ -0,0 +1,34 @@
+const mongoose = require('mongoose');
+
+const notificationSchema = new mongoose.Schema({
+ recipient: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ index: true,
+ },
+ type: {
+ type: String,
+ enum: [
+ 'query_response',
+ 'query_status',
+ 'question_answered',
+ 'question_promoted',
+ 'new_query',
+ 'new_question',
+ 'new_user',
+ 'answer_upvote',
+ 'answer_downvote',
+ ],
+ required: true,
+ },
+ title: { type: String, required: true },
+ message: { type: String, required: true },
+ link: { type: String, default: null },
+ relatedId: { type: mongoose.Schema.Types.ObjectId, default: null },
+ read: { type: Boolean, default: false, index: true },
+}, { timestamps: true });
+
+notificationSchema.index({ recipient: 1, read: 1, createdAt: -1 });
+
+module.exports = mongoose.model('Notification', notificationSchema);
diff --git a/server/models/Question.js b/server/models/Question.js
new file mode 100644
index 0000000..93db4b1
--- /dev/null
+++ b/server/models/Question.js
@@ -0,0 +1,28 @@
+const mongoose = require('mongoose');
+
+const questionSchema = new mongoose.Schema({
+ title: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: false,
+ },
+ status: {
+ type: String,
+ enum: ['pending', 'answered'],
+ default: 'pending',
+ },
+ author: {
+ type: mongoose.Schema.Types.ObjectId,
+ required: true,
+ ref: 'User',
+ },
+ isGoodQuestion: {
+ type: Boolean,
+ default: false,
+ },
+}, { timestamps: true });
+
+module.exports = mongoose.model('Question', questionSchema);
\ No newline at end of file
diff --git a/server/models/User.js b/server/models/User.js
index c6e3ef3..c51f318 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -52,6 +52,22 @@ const userSchema = new mongoose.Schema(
passwordChangedAt: Date,
passwordResetToken: String,
passwordResetExpires: Date,
+
+ // Points system
+ points: {
+ type: Number,
+ default: 0,
+ },
+ pointsHistory: [
+ {
+ action: { type: String },
+ points: { type: Number },
+ referenceId: { type: mongoose.Schema.Types.ObjectId },
+ awardedAt: { type: Date },
+ appliedAt: { type: Date }, // 24hr delay
+ applied: { type: Boolean, default: false },
+ }
+ ],
},
{ timestamps: true }
);
@@ -100,4 +116,22 @@ userSchema.methods.createPasswordResetToken = function () {
return resetToken;
};
-module.exports = mongoose.model('User', userSchema);
+userSchema.methods.awardPoints = async function (action, points, referenceId = null) {
+ const now = new Date();
+
+ this.pointsHistory.push({
+ action,
+ points,
+ referenceId,
+ awardedAt: now,
+ appliedAt: now,
+ applied: true, // mark as already applied
+ });
+
+ // Apply immediately
+ this.points = Math.max(0, (this.points || 0) + points);
+
+ await this.save();
+};
+
+module.exports = mongoose.model('User', userSchema);
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
index 62e6f78..489f2f5 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -26,6 +26,7 @@
"mongoose": "^8.6.0",
"morgan": "^1.10.1",
"puppeteer": "^25.1.0",
+ "socket.io": "^4.8.3",
"validator": "^13.12.0"
}
},
@@ -836,6 +837,21 @@
}
}
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -866,6 +882,15 @@
"@types/webidl-conversions": "*"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@xenova/transformers": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
@@ -1139,6 +1164,15 @@
],
"license": "MIT"
},
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
@@ -1894,6 +1928,80 @@
"once": "^1.4.0"
}
},
+ "node_modules/engine.io": {
+ "version": "6.6.8",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.8.tgz",
+ "integrity": "sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "@types/ws": "^8.5.12",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.20.1"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/engine.io/node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -3859,6 +3967,137 @@
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/socket.io": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+ "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.7",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.7.tgz",
+ "integrity": "sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.4.1",
+ "ws": "~8.20.1"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-adapter/node_modules/ws": {
+ "version": "8.20.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
+ "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
+ "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
diff --git a/server/package.json b/server/package.json
index 017fc08..cc93766 100644
--- a/server/package.json
+++ b/server/package.json
@@ -26,6 +26,7 @@
"mongoose": "^8.6.0",
"morgan": "^1.10.1",
"puppeteer": "^25.1.0",
+ "socket.io": "^4.8.3",
"validator": "^13.12.0"
}
}
diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js
index bcb4cd4..3f465db 100644
--- a/server/routes/adminRoutes.js
+++ b/server/routes/adminRoutes.js
@@ -3,13 +3,40 @@ const adminController = require('../controllers/adminController');
const { authenticateUser, authorizeRoles } = require('../middleware/auth');
const router = Router();
+const isAdmin = authorizeRoles('super_admin', 'admin');
+const isSuperAdmin = authorizeRoles('super_admin');
-router.use(authenticateUser, authorizeRoles('super_admin', 'admin'));
+// Leaderboard β visible to all authenticated users
+router.get('/leaderboard', authenticateUser, adminController.getLeaderboard);
+router.use(authenticateUser, isAdmin);
+
+// Stats, users
+router.get('/stats', adminController.getStats);
router.get('/users', adminController.getUsers);
router.get('/users/:id', adminController.getUserById);
-router.patch('/users/:id/role', authorizeRoles('super_admin', 'admin'), adminController.updateUserRole);
-router.patch('/users/:id/promote', authorizeRoles('super_admin', 'admin'), adminController.promoteToAdmin);
-router.delete('/users/:id', authorizeRoles('super_admin'), adminController.deleteUser);
-module.exports = router;
+// Role management
+router.patch('/users/:id/promote', isAdmin, adminController.promoteToAdmin);
+router.patch('/users/:id/promote-super', isSuperAdmin, adminController.promoteToSuperAdmin);
+router.patch('/users/:id/demote', isSuperAdmin, adminController.demoteAdmin);
+router.patch('/users/:id/role', isSuperAdmin, adminController.updateUserRole);
+router.delete('/users/:id', isAdmin, adminController.deleteUser);
+
+// Questions
+router.patch('/questions/:id/good-question', adminController.markGoodQuestion);
+router.post('/questions/:id/promote-faq', adminController.promoteQuestionToFaq);
+
+// Pending answers moderation
+router.get('/answers/pending', adminController.getPendingAnswers);
+
+// FAQ CRUD (admin / super_admin)
+router.get('/faqs', isAdmin, adminController.getAllFaqs);
+router.post('/faqs', isAdmin, adminController.createFaq);
+router.patch('/faqs/:id', isAdmin, adminController.updateFaq);
+router.delete('/faqs/:id', isAdmin, adminController.deleteFaq);
+
+// Audit log (super_admin only)
+router.get('/audit-logs', isSuperAdmin, adminController.getAuditLogs);
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/routes/answerRoutes.js b/server/routes/answerRoutes.js
new file mode 100644
index 0000000..752318a
--- /dev/null
+++ b/server/routes/answerRoutes.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const router = express.Router();
+const { createAnswer, getAnswersByQuestionId, deleteAnswer, upvoteAnswer, downvoteAnswer, acceptAnswer, approveAnswer, rejectAnswer, getAllAnswers } = require('../controllers/answerController');
+const { authenticateUser: protect, authorizeRoles } = require('../middleware/auth');
+const admin = authorizeRoles('admin', 'super_admin');
+
+router.get('/', protect, admin, getAllAnswers);
+router.post('/', protect, createAnswer);
+router.get('/:questionId', protect, getAnswersByQuestionId);
+router.delete('/:id', protect, admin, deleteAnswer);
+router.put('/:id/upvote', protect, upvoteAnswer);
+router.put('/:id/downvote', protect, downvoteAnswer);
+router.put('/:id/accept', protect, admin, acceptAnswer);
+router.put('/:id/approve', protect, admin, approveAnswer);
+router.put('/:id/reject', protect, admin, rejectAnswer);
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/routes/notificationRoutes.js b/server/routes/notificationRoutes.js
new file mode 100644
index 0000000..6a82d75
--- /dev/null
+++ b/server/routes/notificationRoutes.js
@@ -0,0 +1,19 @@
+const { Router } = require('express');
+const { authenticateUser } = require('../middleware/auth');
+const {
+ getNotifications,
+ getUnreadCount,
+ markAsRead,
+ markAllAsRead,
+} = require('../controllers/notificationController');
+
+const router = Router();
+
+router.use(authenticateUser);
+
+router.get('/', getNotifications);
+router.get('/unread-count', getUnreadCount);
+router.patch('/:id/read', markAsRead);
+router.put('/read-all', markAllAsRead);
+
+module.exports = router;
diff --git a/server/routes/queryRoutes.js b/server/routes/queryRoutes.js
index e8281be..58755b6 100644
--- a/server/routes/queryRoutes.js
+++ b/server/routes/queryRoutes.js
@@ -10,4 +10,8 @@ router.get('/me', authenticateUser, queryController.getMyQueries);
router.get('/', authenticateUser, authorizeRoles('admin', 'super_admin'), queryController.getAllQueries);
router.put('/:id', authenticateUser, authorizeRoles('admin', 'super_admin'), queryController.respondToQuery);
+router.get('/all', authenticateUser, authorizeRoles('admin', 'super_admin'), queryController.getAllQueries);
+router.patch('/:id/respond', authenticateUser, authorizeRoles('admin', 'super_admin'), queryController.respondToQuery);
+router.delete('/:id', authenticateUser, queryController.deleteQuery);
+
module.exports = router;
diff --git a/server/routes/questionRoutes.js b/server/routes/questionRoutes.js
new file mode 100644
index 0000000..f12e30b
--- /dev/null
+++ b/server/routes/questionRoutes.js
@@ -0,0 +1,17 @@
+const express = require('express');
+const router = express.Router();
+const { createQuestion, getQuestions, getQuestionById, getMyQuestions, deleteQuestion } = require('../controllers/questionController');
+const { authenticateUser: protect, authorizeRoles } = require('../middleware/auth');
+const admin = authorizeRoles('admin');
+
+router.route('/')
+ .post(protect, createQuestion)
+ .get(protect, getQuestions);
+
+router.get('/myquestions', protect, getMyQuestions);
+
+router.route('/:id')
+ .get(protect, getQuestionById)
+ .delete(protect, admin, deleteQuestion);
+
+module.exports = router;
diff --git a/server/routes/searchRoutes.js b/server/routes/searchRoutes.js
index 536ff16..d52b8de 100644
--- a/server/routes/searchRoutes.js
+++ b/server/routes/searchRoutes.js
@@ -1,6 +1,8 @@
const { Router } = require('express');
const rateLimit = require('express-rate-limit');
const searchController = require('../controllers/searchController');
+const jwt = require('jsonwebtoken');
+const User = require('../models/User');
const router = Router();
@@ -20,7 +22,23 @@ const suggestionLimiter = rateLimit({
legacyHeaders: false,
});
-router.post('/', searchLimiter, searchController.search);
+// Bypasses authenticateUser entirely β manually decodes token so errors never block the request
+const optionalAuth = async (req, res, next) => {
+ try {
+ const authHeader = req.headers.authorization;
+ if (authHeader && authHeader.startsWith('Bearer ')) {
+ const token = authHeader.split(' ')[1];
+ const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
+ const user = await User.findById(decoded.userId);
+ if (user) req.user = user;
+ }
+ } catch (_) {
+ // invalid/expired token β just skip, don't block
+ }
+ next();
+};
+
+router.post('/', searchLimiter, optionalAuth, searchController.search);
router.get('/suggestions', suggestionLimiter, searchController.suggestions);
-module.exports = router;
+module.exports = router;
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index ebb53c6..70321f1 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,4 +1,5 @@
ο»Ώrequire('dotenv').config({ path: require('path').join(__dirname, '.env') });
+const http = require('http');
const express = require('express');
const mongoose = require('mongoose');
const helmet = require('helmet');
@@ -16,12 +17,27 @@ const faqRoutes = require('./routes/faqRoutes');
const queryRoutes = require('./routes/queryRoutes');
const internshipRoutes = require('./routes/internshipRoutes');
const searchRoutes = require('./routes/searchRoutes');
+const answerRoutes = require('./routes/answerRoutes');
+const questionRoutes = require('./routes/questionRoutes');
+const notificationRoutes = require('./routes/notificationRoutes');
const { indexAllFaqs } = require('./services/searchService');
const { errorHandler } = require('./middleware/errorHandler');
+const { setupSocket } = require('./services/socketService');
const app = express();
-app.use(helmet());
+app.use(helmet({
+ crossOriginOpenerPolicy: false,
+ contentSecurityPolicy: {
+ directives: {
+ defaultSrc: ["'self'"],
+ scriptSrc: ["'self'", "https://accounts.google.com"],
+ frameSrc: ["'self'", "https://accounts.google.com"],
+ connectSrc: ["'self'", "https://accounts.google.com"],
+ imgSrc: ["'self'", "data:", "https://*.googleusercontent.com"],
+ },
+ },
+}));
app.use(cors({
origin: [process.env.CLIENT_URL || 'http://localhost:5173', 'http://localhost:5174'],
credentials: true,
@@ -32,7 +48,7 @@ app.use(mongoSanitize());
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
- max: 100,
+ max: 500,
message: { success: false, message: 'Too many requests. Try again after 15 minutes.' },
standardHeaders: true,
legacyHeaders: false,
@@ -49,6 +65,9 @@ app.use('/api/faqs', faqRoutes);
app.use('/api/queries', queryRoutes);
app.use('/api/internship', internshipRoutes);
app.use('/api/search', searchRoutes);
+app.use('/api/answers', answerRoutes);
+app.use('/api/questions', questionRoutes);
+app.use('/api/notifications', notificationRoutes);
app.all('*', (req, res) => {
res.status(404).json({ success: false, message: `Route ${req.originalUrl} not found` });
@@ -93,6 +112,9 @@ const seedFAQs = async () => {
}
};
+const server = http.createServer(app);
+setupSocket(server);
+
const startServer = async () => {
try {
await connectDB();
@@ -100,9 +122,32 @@ const startServer = async () => {
indexAllFaqs().then(count => {
if (count > 0) console.log(`Search: Indexed ${count} FAQs for semantic search`);
}).catch(() => {});
- app.listen(PORT, () => {
+ server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
+// Apply pending points every hour
+setInterval(async () => {
+ try {
+ const User = require('./models/User');
+ const now = new Date();
+ const users = await User.find({ 'pointsHistory.applied': false });
+ for (const user of users) {
+ let changed = false;
+ for (const entry of user.pointsHistory) {
+ if (!entry.applied && entry.appliedAt <= now) {
+ user.points = Math.max(0, (user.points || 0) + entry.points);
+ entry.applied = true;
+ changed = true;
+ }
+ }
+ if (changed) await user.save();
+ }
+ console.log('Points job ran successfully');
+ } catch (e) {
+ console.warn('Points job error:', e.message);
+ }
+}, 60 * 60 * 1000);
+
} catch (err) {
console.error('Failed to start server:', err.message);
process.exit(1);
@@ -110,3 +155,25 @@ const startServer = async () => {
};
startServer();
+
+setInterval(async () => {
+ try {
+ const User = require('./models/User');
+ const now = new Date();
+ const users = await User.find({ 'pointsHistory.applied': false });
+
+ for (const user of users) {
+ let changed = false;
+ for (const entry of user.pointsHistory) {
+ if (!entry.applied && entry.appliedAt <= now) {
+ user.points = Math.max(0, (user.points || 0) + entry.points);
+ entry.applied = true;
+ changed = true;
+ }
+ }
+ if (changed) await user.save();
+ }
+ } catch (e) {
+ console.warn('Points job error:', e.message);
+ }
+}, 10 * 1000);
diff --git a/server/services/searchService.js b/server/services/searchService.js
index 8fb0804..18f8b8b 100644
--- a/server/services/searchService.js
+++ b/server/services/searchService.js
@@ -4,6 +4,12 @@ const Faq = require('../models/Faq');
const { generateEmbedding } = require('./embeddingService');
const { fetchOverview } = require('./internshipOverview');
+class NoOpEmbeddingFunction {
+ async generate(texts) {
+ return texts.map(() => []);
+ }
+}
+
const CHROMA_HOST = process.env.CHROMA_HOST || 'localhost';
const CHROMA_PORT = parseInt(process.env.CHROMA_PORT, 10) || 8000;
const CHROMA_COLLECTION = process.env.CHROMA_COLLECTION || 'faq_embeddings';
@@ -16,6 +22,9 @@ let chromaClient = null;
let collection = null;
let chromaAvailable = false;
+// In-memory embeddings cache
+const faqEmbeddingsCache = new Map();
+
function initChroma() {
if (chromaClient) return;
try {
@@ -30,7 +39,7 @@ async function ensureCollection() {
initChroma();
if (!chromaClient) return null;
try {
- collection = await chromaClient.getOrCreateCollection({ name: CHROMA_COLLECTION });
+ collection = await chromaClient.getOrCreateCollection({ name: CHROMA_COLLECTION, embeddingFunction: new NoOpEmbeddingFunction() });
chromaAvailable = true;
return collection;
} catch {
@@ -50,12 +59,40 @@ function cosineSimilarity(a, b) {
return denom === 0 ? 0 : dot / denom;
}
+async function buildEmbeddingsCache() {
+ const faqs = await Faq.find({}).select('_id question answer category').lean();
+ console.log(`Building embeddings cache for ${faqs.length} FAQs...`);
+ for (const faq of faqs) {
+ try {
+ const text = `${faq.question} ${faq.answer}`;
+ const embedding = await generateEmbedding(text);
+ faqEmbeddingsCache.set(faq._id.toString(), {
+ faqId: faq._id.toString(),
+ question: faq.question,
+ category: faq.category,
+ document: text,
+ embedding,
+ });
+ } catch { /* skip */ }
+ }
+ console.log(`Embeddings cache built: ${faqEmbeddingsCache.size} FAQs`);
+}
+
async function indexFaq(faq) {
if (!faq || !faq._id) throw new Error('Valid FAQ with _id is required');
const text = `${faq.question} ${faq.answer}`;
const embedding = await generateEmbedding(text);
+ // Update cache
+ faqEmbeddingsCache.set(faq._id.toString(), {
+ faqId: faq._id.toString(),
+ question: faq.question,
+ category: faq.category,
+ document: text,
+ embedding,
+ });
+
const col = await ensureCollection();
if (col) {
try {
@@ -77,6 +114,7 @@ async function indexFaq(faq) {
}
async function deleteFaqIndex(faqId) {
+ faqEmbeddingsCache.delete(faqId.toString());
const col = await ensureCollection();
if (col) {
try {
@@ -88,9 +126,7 @@ async function deleteFaqIndex(faqId) {
}
async function searchSimilar(query, limit = 5) {
- if (!query || query.trim().length < 3) {
- return [];
- }
+ if (!query || query.trim().length < 3) return [];
const queryEmbedding = await generateEmbedding(query);
const col = await ensureCollection();
@@ -123,24 +159,21 @@ async function searchSimilar(query, limit = 5) {
}
}
- const faqs = await Faq.find({ isPublished: true }).select('_id question answer category').lean();
- const scored = [];
+ // Use cache instead of recomputing every time
+ if (faqEmbeddingsCache.size === 0) {
+ await buildEmbeddingsCache();
+ }
- for (const faq of faqs) {
- const text = `${faq.question} ${faq.answer}`;
- try {
- const faqEmbedding = await generateEmbedding(text);
- const score = cosineSimilarity(queryEmbedding, faqEmbedding);
- scored.push({
- faqId: faq._id.toString(),
- question: faq.question,
- category: faq.category,
- document: `${faq.question} ${faq.answer}`,
- score: Math.round(score * 100) / 100,
- });
- } catch {
- // skip individual FAQ on embedding error
- }
+ const scored = [];
+ for (const [, faq] of faqEmbeddingsCache) {
+ const score = cosineSimilarity(queryEmbedding, faq.embedding);
+ scored.push({
+ faqId: faq.faqId,
+ question: faq.question,
+ category: faq.category,
+ document: faq.document,
+ score: Math.round(score * 100) / 100,
+ });
}
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
@@ -155,6 +188,54 @@ async function getSuggestions(query) {
}));
}
+async function generateGeneralAnswer(userQuery) {
+ if (!LLM_API_KEY) {
+ return {
+ answer: "Hello! How can I help you today?",
+ confidence: 0,
+ };
+ }
+
+ try {
+ const { data } = await axios.post(
+ LLM_ENDPOINT,
+ {
+ model: LLM_MODEL,
+ messages: [
+ {
+ role: 'system',
+ content:
+ 'You are a friendly chatbot. Respond naturally to greetings, small talk, and general conversation. Keep responses short.',
+ },
+ {
+ role: 'user',
+ content: userQuery,
+ },
+ ],
+ temperature: 0.7,
+ max_tokens: 150,
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${LLM_API_KEY}`,
+ },
+ timeout: 10000,
+ }
+ );
+
+ return {
+ answer: data.choices?.[0]?.message?.content?.trim() || 'Hello!',
+ confidence: 0,
+ };
+ } catch {
+ return {
+ answer: 'Hello! How can I help you today?',
+ confidence: 0,
+ };
+ }
+}
+
async function generateAnswer(userQuery, sources) {
if (!sources || sources.length === 0) {
return {
@@ -169,6 +250,9 @@ async function generateAnswer(userQuery, sources) {
const avgScore = sources.reduce((sum, s) => sum + s.score, 0) / sources.length;
const confidence = Math.round(avgScore * 100) / 100;
+ console.log('Query:', userQuery);
+console.log('Top score:', sources[0]?.score);
+console.log('Confidence:', confidence);
if (!LLM_API_KEY) {
const best = sources[0];
@@ -180,7 +264,6 @@ async function generateAnswer(userQuery, sources) {
}
const systemPrompt = `You are a helpful FAQ assistant. Answer the user's question based only on the provided context. Be concise and direct. If the context has related information, use it to answer even if the wording is different.`;
-
const userPrompt = `Context:\n${context}\n\nQuestion: ${userQuery}\n\nAnswer based only on the context above.`;
try {
@@ -217,24 +300,23 @@ async function generateAnswer(userQuery, sources) {
}
async function indexAllFaqs() {
- const faqs = await Faq.find({ isPublished: true }).lean();
+ const faqs = await Faq.find({}).lean();
let indexed = 0;
for (const faq of faqs) {
try {
await indexFaq(faq);
indexed++;
- } catch {
- // skip individual failures during bulk index
- }
+ } catch { /* skip */ }
}
+ // Build in-memory cache on startup
+ await buildEmbeddingsCache();
+
try {
await indexOverview();
indexed++;
- } catch {
- // overview index failed, non-critical
- }
+ } catch { /* non-critical */ }
return indexed;
}
@@ -278,7 +360,7 @@ async function indexOverview() {
metadatas: [{ faqId: sec.id, question: sec.title, category: 'programme-overview' }],
documents: [text],
});
- } catch { /* skip individual */ }
+ } catch { /* skip */ }
}
}
}
@@ -289,6 +371,8 @@ module.exports = {
searchSimilar,
getSuggestions,
generateAnswer,
+ generateGeneralAnswer,
indexAllFaqs,
indexOverview,
-};
+ buildEmbeddingsCache,
+};
\ No newline at end of file
diff --git a/server/services/socketService.js b/server/services/socketService.js
new file mode 100644
index 0000000..c7f2a4c
--- /dev/null
+++ b/server/services/socketService.js
@@ -0,0 +1,66 @@
+const jwt = require('jsonwebtoken');
+const User = require('../models/User');
+
+let io = null;
+
+const connectedUsers = new Map();
+
+function setupSocket(server) {
+ const { Server } = require('socket.io');
+
+ io = new Server(server, {
+ cors: {
+ origin: [process.env.CLIENT_URL || 'http://localhost:5173', 'http://localhost:5174'],
+ credentials: true,
+ },
+ });
+
+ io.use(async (socket, next) => {
+ try {
+ const token = socket.handshake.auth?.token || socket.handshake.query?.token;
+ if (!token) {
+ return next(new Error('Authentication required'));
+ }
+ const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET);
+ const user = await User.findById(decoded.userId).select('_id role name email');
+ if (!user) {
+ return next(new Error('User not found'));
+ }
+ socket.user = user;
+ next();
+ } catch {
+ next(new Error('Invalid token'));
+ }
+ });
+
+ io.on('connection', (socket) => {
+ const userId = socket.user._id.toString();
+ socket.join(`user:${userId}`);
+ connectedUsers.set(userId, socket.id);
+
+ socket.on('disconnect', () => {
+ connectedUsers.delete(userId);
+ });
+ });
+
+ return io;
+}
+
+function getIO() {
+ if (!io) throw new Error('Socket.io not initialized');
+ return io;
+}
+
+function notifyUser(userId, notification) {
+ if (io) {
+ io.to(`user:${userId}`).emit('notification', notification);
+ }
+}
+
+function notifyAdmins(notification) {
+ if (io) {
+ io.emit('admin_notification', notification);
+ }
+}
+
+module.exports = { setupSocket, getIO, notifyUser, notifyAdmins };
diff --git a/server_err.txt b/server_err.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/server_err2.txt b/server_err2.txt
deleted file mode 100644
index bf947e5..0000000
--- a/server_err2.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
diff --git a/server_err3.txt b/server_err3.txt
deleted file mode 100644
index bf947e5..0000000
--- a/server_err3.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
diff --git a/server_err4.txt b/server_err4.txt
deleted file mode 100644
index bf947e5..0000000
--- a/server_err4.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
diff --git a/server_err5.txt b/server_err5.txt
deleted file mode 100644
index bf947e5..0000000
--- a/server_err5.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
diff --git a/server_err6.txt b/server_err6.txt
deleted file mode 100644
index bf947e5..0000000
--- a/server_err6.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
-Cannot instantiate a collection with the DefaultEmbeddingFunction. Please install @chroma-core/default-embed, or provide a different embedding function
diff --git a/server_out.txt b/server_out.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/server_out2.txt b/server_out2.txt
deleted file mode 100644
index 28df214..0000000
--- a/server_out2.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-MongoDB connected: ac-ahewmjg-shard-00-00.8hiyp5g.mongodb.net
-FAQs: 132 internship FAQs already exist
-Server running on http://localhost:3000
-Search: Indexed 132 FAQs for semantic search
diff --git a/server_out3.txt b/server_out3.txt
deleted file mode 100644
index 28df214..0000000
--- a/server_out3.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-MongoDB connected: ac-ahewmjg-shard-00-00.8hiyp5g.mongodb.net
-FAQs: 132 internship FAQs already exist
-Server running on http://localhost:3000
-Search: Indexed 132 FAQs for semantic search
diff --git a/server_out4.txt b/server_out4.txt
deleted file mode 100644
index 28df214..0000000
--- a/server_out4.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-MongoDB connected: ac-ahewmjg-shard-00-00.8hiyp5g.mongodb.net
-FAQs: 132 internship FAQs already exist
-Server running on http://localhost:3000
-Search: Indexed 132 FAQs for semantic search
diff --git a/server_out5.txt b/server_out5.txt
deleted file mode 100644
index 28df214..0000000
--- a/server_out5.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-MongoDB connected: ac-ahewmjg-shard-00-00.8hiyp5g.mongodb.net
-FAQs: 132 internship FAQs already exist
-Server running on http://localhost:3000
-Search: Indexed 132 FAQs for semantic search
diff --git a/server_out6.txt b/server_out6.txt
deleted file mode 100644
index 28df214..0000000
--- a/server_out6.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-MongoDB connected: ac-ahewmjg-shard-00-00.8hiyp5g.mongodb.net
-FAQs: 132 internship FAQs already exist
-Server running on http://localhost:3000
-Search: Indexed 132 FAQs for semantic search
diff --git a/server_run.err b/server_run.err
deleted file mode 100644
index 975a7cc..0000000
--- a/server_run.err
+++ /dev/null
@@ -1,3 +0,0 @@
-The 'path' argument is deprecated. Please use 'ssl', 'host', and 'port' instead
-RAG: ChromaDB not available (start with: chroma run --path ./chroma_data)
-RAG: Falling back to in-memory search
diff --git a/start_server.ps1 b/start_server.ps1
deleted file mode 100644
index de9083a..0000000
--- a/start_server.ps1
+++ /dev/null
@@ -1,17 +0,0 @@
-$log = "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server_run.log"
-$err = "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server_run.err"
-$psi = New-Object System.Diagnostics.ProcessStartInfo
-$psi.FileName = "node"
-$psi.Arguments = "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server\server.js"
-$psi.UseShellExecute = $false
-$psi.RedirectStandardOutput = $true
-$psi.RedirectStandardError = $true
-$p = [System.Diagnostics.Process]::Start($psi)
-$p.Id | Out-File -LiteralPath "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server_pid.txt"
-Start-Sleep -Seconds 5
-if (!$p.HasExited) {
- "Running PID: $($p.Id)" | Out-File -LiteralPath "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server_status.txt"
-} else {
- "Exited with code: $($p.ExitCode)" | Out-File -LiteralPath "C:\Users\beldh\OneDrive\Desktop\FAQ_SYSTEM\server_status.txt"
- $p.StandardError.ReadToEnd() | Out-File -LiteralPath $err
-}