├── frontend └── aiict.in │ └── admin │ ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html │ ├── src │ ├── config.js │ ├── setupTests.js │ ├── index.js │ ├── index.css │ ├── reportWebVitals.js │ ├── App.css │ ├── App.js │ ├── components │ │ ├── Navbar.js │ │ └── ProtectedRoute.js │ ├── logo.svg │ ├── pages │ │ ├── Login.js │ │ ├── FeeManagement.js │ │ ├── Dashboard.js │ │ └── StudentList.js │ └── services │ │ └── api.js │ ├── .env │ └── package.json ├── database └── schema.sql ├── backend ├── .env ├── .env.local ├── src │ ├── models │ │ └── Fee.js │ ├── utils │ │ ├── database.js │ │ ├── sms-cost-optimizer.js │ │ ├── auth-middleware.js │ │ ├── notification-logs.js │ │ ├── notification.js │ │ ├── simple-logs.js │ │ └── notification-v2.js │ ├── jobs │ │ └── reminderJob.js │ ├── handlers │ │ ├── export.js │ │ ├── auth.js │ │ ├── fixed-export.js │ │ ├── student.js │ │ ├── stats.js │ │ ├── fees.js │ │ ├── multi-export.js │ │ ├── reminders.js │ │ └── notifications.js │ └── app.js ├── utils │ ├── generate-password.js │ ├── generate-admin-password.js │ ├── server.js │ └── local-server.js ├── package.json ├── tests │ ├── remove-test-student.js │ ├── check-students.js │ ├── trigger-reminder.js │ ├── direct-sms-test.js │ ├── create-test-student-prod.js │ ├── create-test-student-fixed.js │ ├── create-test-student.js │ ├── check-endpoints.js │ ├── check-reminder-eligibility.js │ ├── test-sms-direct.js │ ├── test-sms-server.js │ ├── test-reminder-system.js │ └── test-endpoints.js └── serverless.yml ├── .gitignore └── README.md /frontend/aiict.in/admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lakshit-Gupta/fee-management-system/HEAD/frontend/aiict.in/admin/public/favicon.ico -------------------------------------------------------------------------------- /frontend/aiict.in/admin/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lakshit-Gupta/fee-management-system/HEAD/frontend/aiict.in/admin/public/logo192.png -------------------------------------------------------------------------------- /frontend/aiict.in/admin/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lakshit-Gupta/fee-management-system/HEAD/frontend/aiict.in/admin/public/logo512.png -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/config.js: -------------------------------------------------------------------------------- 1 | // Create file at: src/config.js 2 | export const API_BASE_URL = process.env.NODE_ENV === 'development' 3 | ? 'http://localhost:4000/dev' // Development server 4 | : 'YOUR_PRODUCTION_API_ENDPOINT'; // Production server -------------------------------------------------------------------------------- /frontend/aiict.in/admin/.env: -------------------------------------------------------------------------------- 1 | # React environment variables for production 2 | REACT_APP_API_URL= #SOMETHING LIKE THIS IF UPLOADED ON AWS https://.execute-api..amazonaws.com/live 3 | REACT_APP_STAGE=live 4 | GENERATE_SOURCEMAP=false 5 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /database/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE Students ( 2 | id SERIAL PRIMARY KEY, 3 | name VARCHAR(100) NOT NULL, 4 | fathers_name VARCHAR(100) NOT NULL, 5 | registration_no VARCHAR(50) UNIQUE NOT NULL, 6 | phone_no VARCHAR(15) NOT NULL, 7 | course_name VARCHAR(100) NOT NULL, 8 | batch_time TIME NOT NULL 9 | ); 10 | 11 | CREATE TABLE Fees ( 12 | id SERIAL PRIMARY KEY, 13 | student_id INT REFERENCES Students(id), 14 | amount DECIMAL(10, 2) NOT NULL, 15 | due_date DATE NOT NULL, 16 | payment_status BOOLEAN DEFAULT FALSE 17 | ); -------------------------------------------------------------------------------- /frontend/aiict.in/admin/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | # Fast2SMS Configuration 2 | FAST2SMS_API_KEY= 3 | 4 | # SMS Configuration 5 | SMS_ENABLED=true 6 | NOTIFICATION_PROVIDER=fast2sms #YOUR SMS PROVIDER 7 | DEFAULT_CHANNELS=sms 8 | 9 | # App Configuration 10 | APP_NAME= 11 | INSTITUTE_NAME= 12 | OWNER_PHONE= 13 | SUPPORT_PHONE= 14 | PAYMENT_URL= 15 | 16 | # App Configuration 17 | NODE_ENV=production 18 | CORS_ORIGIN= 19 | 20 | # Security Configuration 21 | JWT_SECRET= 22 | ADMIN_EMAIL= 23 | ADMIN_PASSWORD_HASH= 24 | PASSWORD_SALT= 25 | 26 | -------------------------------------------------------------------------------- /backend/.env.local: -------------------------------------------------------------------------------- 1 | # Local development environment variables 2 | 3 | # AWS Configuration 4 | AWS_REGION= 5 | FAST2SMS_API_KEY= 6 | 7 | # Authentication 8 | JWT_SECRET= 9 | ADMIN_EMAIL= 10 | ADMIN_PASSWORD_HASH= 11 | PASSWORD_SALT= 12 | 13 | # CORS 14 | CORS_ORIGIN=http://localhost:3001 #LOCAL SERVER TESTING 15 | 16 | # Fast2SMS Configuration 17 | NOTIFICATION_PROVIDER=fast2sms #YOUR SMS PROVIDER 18 | SMS_ENABLED=true 19 | FAST2SMS_API_KEY= 20 | 21 | # Database tables (local version) FOR TESTING 22 | STUDENTS_TABLE=Students-local 23 | FEES_TABLE=Fees-local 24 | NOTIFICATION_LOGS_TABLE=NotificationLogs-local 25 | 26 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/models/Fee.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize'); 2 | const sequelize = require('../utils/database'); 3 | 4 | class Fee extends Model {} 5 | 6 | Fee.init({ 7 | amount: { 8 | type: DataTypes.FLOAT, 9 | allowNull: false 10 | }, 11 | dueDate: { 12 | type: DataTypes.DATE, 13 | allowNull: false 14 | }, 15 | paymentStatus: { 16 | type: DataTypes.ENUM('paid', 'due'), 17 | defaultValue: 'due' 18 | }, 19 | studentId: { 20 | type: DataTypes.INTEGER, 21 | allowNull: false, 22 | references: { 23 | model: 'Students', 24 | key: 'id' 25 | } 26 | } 27 | }, { 28 | sequelize, 29 | modelName: 'Fee' 30 | }); 31 | 32 | module.exports = Fee; -------------------------------------------------------------------------------- /backend/src/utils/database.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | // Configure AWS SDK 4 | AWS.config.update({ 5 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 6 | }); 7 | 8 | // Create DynamoDB table if it doesn't exist 9 | const dynamodb = new AWS.DynamoDB(); 10 | 11 | const createTableIfNotExists = async () => { 12 | try { 13 | await dynamodb.describeTable({ TableName: 'Students' }).promise(); 14 | console.log('Students table already exists'); 15 | } catch (error) { 16 | if (error.code === 'ResourceNotFoundException') { 17 | // Create table using the setup script 18 | const { createStudentTable } = require('./dynamodb-setup'); 19 | await createStudentTable(); 20 | console.log('Students table created successfully'); 21 | } else { 22 | throw error; 23 | } 24 | } 25 | }; 26 | 27 | module.exports = { createTableIfNotExists }; -------------------------------------------------------------------------------- /frontend/aiict.in/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/dom": "^10.4.0", 7 | "@testing-library/jest-dom": "^6.6.3", 8 | "@testing-library/react": "^16.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.10.0", 11 | "bootstrap": "^5.3.7", 12 | "react": "^19.1.0", 13 | "react-bootstrap": "^2.10.10", 14 | "react-dom": "^19.1.0", 15 | "react-router-dom": "^7.6.2", 16 | "react-scripts": "5.0.1", 17 | "react-toastify": "^11.0.5", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/utils/generate-password.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This utility generates a secure password hash with salt 3 | * Use this to create a new admin password hash for the system 4 | * 5 | * Usage: 6 | * node generate-password.js YourDesiredPassword 7 | */ 8 | 9 | const crypto = require('crypto'); 10 | 11 | // The salt should match what's in auth.js / environment variables 12 | const SALT = process.env.PASSWORD_SALT || 'YOUR_SALT'; 13 | 14 | if (process.argv.length < 3) { 15 | console.error('Please provide a password to hash.'); 16 | console.error('Usage: node generate-password.js YourPassword'); 17 | process.exit(1); 18 | } 19 | 20 | const password = process.argv[2]; 21 | 22 | // Hash the password with the salt 23 | const hash = crypto.createHash('sha256').update(password + SALT).digest('hex'); 24 | 25 | console.log('\n=== Admin Password Generator ==='); 26 | console.log('\nYour password hash:'); 27 | console.log(hash); 28 | console.log('\nTo use this password in production:'); 29 | console.log('1. Set ADMIN_PASSWORD_HASH environment variable to this hash'); 30 | console.log('2. Ensure PASSWORD_SALT environment variable matches what was used to generate this hash'); 31 | console.log('\n=========================================\n'); 32 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fee-management-system-backend", 3 | "version": "1.0.0", 4 | "description": "Backend for the fee management system with SMS notifications via Fast2SMS", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "start": "node src/app.js", 8 | "dev": "serverless offline", 9 | "deploy": "serverless deploy", 10 | "test": "echo \"No tests specified\" && exit 0", 11 | "simple": "node utils/simple-server.js", 12 | "local": "node utils/local-server.js", 13 | "generate-password": "node utils/generate-password.js", 14 | "generate-admin-password": "node utils/generate-admin-password.js" 15 | }, 16 | "dependencies": { 17 | "aws-sdk": "^2.1692.0", 18 | "axios": "^1.11.0", 19 | "cookie-parser": "^1.4.6", 20 | "cors": "^2.8.5", 21 | "dotenv": "^17.2.1", 22 | "exceljs": "^4.4.0", 23 | "express": "^4.18.2", 24 | "helmet": "^7.1.0", 25 | "jsonwebtoken": "^9.0.2", 26 | "moment": "^2.30.1", 27 | "node-cron": "^3.0.2", 28 | "serverless-http": "^3.2.0", 29 | "uuid": "^9.0.1" 30 | }, 31 | "devDependencies": { 32 | "nodemon": "^3.0.1", 33 | "serverless": "^3.38.0", 34 | "serverless-dotenv-plugin": "^6.0.0", 35 | "serverless-offline": "^13.9.0" 36 | }, 37 | "author": "Your Name", 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | .env.local 4 | .env.production 5 | .env.development 6 | .env.* 7 | 8 | # Dependencies 9 | node_modules/ 10 | */*/node_modules/ 11 | frontend/*/admin/node_modules/ 12 | 13 | # AWS deployment artifacts 14 | .serverless/ 15 | */.serverless/ 16 | 17 | # Build artifacts 18 | build/ 19 | dist/ 20 | */*/build/ 21 | frontend/*/admin/build/ 22 | 23 | # Fast2SMS credentials (never commit) 24 | fast2sms-credentials.json 25 | *-credentials.json 26 | 27 | # Logs 28 | *.log 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # Runtime data 34 | pids 35 | *.pid 36 | *.seed 37 | *.pid.lock 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage/ 41 | 42 | # Dependency directories 43 | jspm_packages/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # IDE files 58 | .vscode/ 59 | .idea/ 60 | *.swp 61 | *.swo 62 | 63 | # Nested Git repositories 64 | frontend/*/.git/ 65 | */*/.git/ 66 | **/.git/ 67 | 68 | # OS generated files 69 | .DS_Store 70 | .DS_Store? 71 | ._* 72 | .Spotlight-V100 73 | .Trashes 74 | ehthumbs.db 75 | Thumbs.db 76 | 77 | # Test files 78 | *test*.json 79 | *test*.xlsx 80 | *test*.csv 81 | **/tests/*.json 82 | **/tests/*.xlsx 83 | **/tests/*.csv 84 | -------------------------------------------------------------------------------- /backend/tests/remove-test-student.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const uuid = require('uuid'); 3 | 4 | // Configure AWS 5 | AWS.config.update({ 6 | region: 'YOUR_AWS_REGION' 7 | }); 8 | 9 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 10 | const tableName = 'Students-pro'; 11 | 12 | async function removeTestStudent() { 13 | try { 14 | // Find the test student by name 15 | const params = { 16 | TableName: tableName, 17 | FilterExpression: "contains(#name, :nameValue)", 18 | ExpressionAttributeNames: { 19 | "#name": "name" 20 | }, 21 | ExpressionAttributeValues: { 22 | ":nameValue": "TEST STUDENT" 23 | } 24 | }; 25 | 26 | const result = await dynamodb.scan(params).promise(); 27 | 28 | if (result.Items.length === 0) { 29 | console.log("No test students found."); 30 | return; 31 | } 32 | 33 | console.log(`Found ${result.Items.length} test students.`); 34 | 35 | // Delete each test student 36 | for (const student of result.Items) { 37 | const deleteParams = { 38 | TableName: tableName, 39 | Key: { 40 | id: student.id 41 | } 42 | }; 43 | 44 | await dynamodb.delete(deleteParams).promise(); 45 | console.log(`Deleted test student with ID: ${student.id}`); 46 | } 47 | 48 | console.log("All test students have been removed successfully."); 49 | } catch (error) { 50 | console.error("Error:", error); 51 | } 52 | } 53 | 54 | removeTestStudent(); 55 | -------------------------------------------------------------------------------- /backend/src/jobs/reminderJob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file handles scheduled reminder jobs for the fee management system 3 | * It uses Node.js setInterval for local development 4 | * In production, AWS EventBridge will trigger the lambda functions instead 5 | */ 6 | const { checkDueFees } = require('../handlers/fees'); 7 | 8 | // Initialize scheduled jobs (only used for local development) 9 | const init = () => { 10 | console.log('Initializing reminder job scheduler...'); 11 | 12 | // For local development only - run the check every day at 7 AM 13 | // In production, AWS EventBridge will trigger the Lambda instead 14 | setInterval(() => { 15 | const now = new Date(); 16 | // Run at 7:00 AM every day (local time) 17 | if (now.getHours() === 7 && now.getMinutes() === 0) { 18 | console.log('Running scheduled fee check...'); 19 | checkDueFees() 20 | .then(result => console.log('Scheduled fee check completed:', result)) 21 | .catch(err => console.error('Error in scheduled fee check:', err)); 22 | } 23 | }, 60 * 1000); // Check every minute 24 | 25 | // Run once at startup during development to test 26 | if (process.env.NODE_ENV === 'development') { 27 | console.log('Development mode - running initial fee check...'); 28 | checkDueFees() 29 | .then(result => console.log('Initial fee check completed:', result)) 30 | .catch(err => console.error('Error in initial fee check:', err)); 31 | } 32 | }; 33 | 34 | module.exports = { 35 | init 36 | }; 37 | -------------------------------------------------------------------------------- /backend/tests/check-students.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | // Configure AWS 4 | AWS.config.update({ 5 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 6 | }); 7 | 8 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 9 | 10 | const checkStudents = async () => { 11 | try { 12 | console.log('Checking all students in database...'); 13 | 14 | const params = { 15 | TableName: process.env.STUDENTS_TABLE || 'Students' 16 | }; 17 | 18 | const result = await dynamodb.scan(params).promise(); 19 | console.log(`Found ${result.Items.length} students total`); 20 | 21 | result.Items.forEach((student, index) => { 22 | console.log(`\n--- Student ${index + 1} ---`); 23 | console.log('ID:', student.id); 24 | console.log('Name:', student.name); 25 | console.log('Phone:', student.phone_no || student.phone); 26 | console.log('Fee Status:', student.fees?.status); 27 | console.log('Due Date:', student.fees?.due_date); 28 | console.log('Fee Amount:', student.fees?.monthly_amount); 29 | }); 30 | 31 | // Check specifically for our test student 32 | const testStudent = result.Items.find(s => s.phone_no === '+91YOUR_TEST_PHONE' || s.phone === '+91YOUR_TEST_PHONE'); 33 | if (testStudent) { 34 | console.log('\n Found our test student for cron testing!'); 35 | console.log('Full student data:', JSON.stringify(testStudent, null, 2)); 36 | } else { 37 | console.log('\n Test student not found!'); 38 | } 39 | 40 | } catch (error) { 41 | console.error(' Error checking students:', error); 42 | } 43 | }; 44 | 45 | checkStudents(); 46 | -------------------------------------------------------------------------------- /backend/tests/trigger-reminder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Trigger Fee Reminder Manually 3 | * 4 | * This script manually invokes the Lambda function that sends SMS reminders. 5 | * Useful for testing without waiting for the scheduled cron job. 6 | */ 7 | 8 | // Load environment variables 9 | require('dotenv').config({ path: '.env.local' }); 10 | 11 | const AWS = require('aws-sdk'); 12 | 13 | // Configure AWS 14 | AWS.config.update({ 15 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 16 | }); 17 | 18 | const lambda = new AWS.Lambda(); 19 | 20 | async function triggerFeeReminder() { 21 | console.log('Manually triggering fee reminder Lambda function...'); 22 | 23 | const params = { 24 | FunctionName: 'YOUR_LAMBDA_FUNCTION_NAME', 25 | InvocationType: 'RequestResponse', // Wait for result 26 | Payload: JSON.stringify({ 27 | source: 'manual-trigger', 28 | timestamp: new Date().toISOString() 29 | }) 30 | }; 31 | 32 | try { 33 | const result = await lambda.invoke(params).promise(); 34 | 35 | console.log('\n Lambda function executed with status:', result.StatusCode); 36 | 37 | if (result.Payload) { 38 | const payload = JSON.parse(result.Payload); 39 | console.log('Response:', JSON.stringify(payload, null, 2)); 40 | } 41 | 42 | return { 43 | success: true, 44 | statusCode: result.StatusCode, 45 | response: result.Payload ? JSON.parse(result.Payload) : null 46 | }; 47 | } catch (error) { 48 | console.error(' ERROR triggering Lambda function:', error); 49 | return { 50 | success: false, 51 | error: error.message 52 | }; 53 | } 54 | } 55 | 56 | // Run the function 57 | triggerFeeReminder(); 58 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { ToastContainer } from 'react-toastify'; 4 | import 'react-toastify/dist/ReactToastify.css'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import './App.css'; 7 | 8 | import Dashboard from './pages/Dashboard'; 9 | import StudentList from './pages/StudentList'; 10 | import AddStudent from './pages/AddStudent'; 11 | import EditStudent from './pages/EditStudent'; 12 | import FeeManagement from './pages/FeeManagement'; 13 | import Reminders from './pages/Reminders'; 14 | import Navbar from './components/Navbar'; 15 | import Login from './pages/Login'; 16 | import ProtectedRoute from './components/ProtectedRoute'; 17 | 18 | function App() { 19 | return ( 20 | 21 |
22 | 23 | 24 |
25 | 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default App; -------------------------------------------------------------------------------- /frontend/aiict.in/admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | // Create file at: src/components/Navbar.js 2 | import React from 'react'; 3 | import { Navbar, Nav, Container, Button } from 'react-bootstrap'; 4 | import { Link, useNavigate } from 'react-router-dom'; 5 | 6 | const AppNavbar = () => { 7 | const navigate = useNavigate(); 8 | const isAuthenticated = localStorage.getItem('authToken') !== null; 9 | const user = JSON.parse(localStorage.getItem('user') || '{}'); 10 | 11 | const handleLogout = () => { 12 | // Clear all auth-related items 13 | localStorage.removeItem('authToken'); 14 | localStorage.removeItem('user'); 15 | localStorage.removeItem('authTokenExpiry'); 16 | 17 | // Redirect to login page 18 | navigate('/login'); 19 | }; 20 | 21 | return ( 22 | 23 | 24 | Fee Management System 25 | 26 | 27 | {isAuthenticated ? ( 28 | <> 29 | 35 | 43 | 44 | ) : ( 45 | 48 | )} 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default AppNavbar; -------------------------------------------------------------------------------- /backend/utils/generate-admin-password.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Password Generator for Admin Panel 3 | * 4 | * Run this script to generate a new password: 5 | * node generate-admin-password.js [new-password] 6 | * 7 | * If no password is provided, a secure random password will be generated. 8 | */ 9 | 10 | const crypto = require('crypto'); 11 | 12 | // Configuration (should match auth.js) 13 | const SALT = process.env.PASSWORD_SALT || 'YOUR_SALT'; 14 | const DEFAULT_PASSWORD_LENGTH = 12; 15 | 16 | // Generate a secure random password if none provided 17 | function generateSecurePassword(length = DEFAULT_PASSWORD_LENGTH) { 18 | // Characters to use in the password (alphanumeric + special chars) 19 | const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+'; 20 | let password = ''; 21 | 22 | // Generate random bytes and map to characters 23 | const randomBytes = crypto.randomBytes(length); 24 | for (let i = 0; i < length; i++) { 25 | const randomIndex = randomBytes[i] % chars.length; 26 | password += chars.charAt(randomIndex); 27 | } 28 | 29 | return password; 30 | } 31 | 32 | // Hash a password with salt 33 | function hashPassword(password, salt) { 34 | return crypto.createHash('sha256').update(password + salt).digest('hex'); 35 | } 36 | 37 | // Main execution 38 | const newPassword = process.argv[2] || generateSecurePassword(); 39 | const hashedPassword = hashPassword(newPassword, SALT); 40 | 41 | console.log('\n=== Admin Password Generator ===\n'); 42 | 43 | if (!process.argv[2]) { 44 | console.log(`Generated secure password: ${newPassword}`); 45 | console.log('SAVE THIS PASSWORD! It will not be shown again.\n'); 46 | } else { 47 | console.log(`Using provided password: ${newPassword}\n`); 48 | } 49 | 50 | console.log('Password hash:'); 51 | console.log(hashedPassword); 52 | 53 | console.log('\nTo update your password:'); 54 | console.log('1. Edit src/handlers/auth.js and update ADMIN_PASSWORD_HASH with this hash'); 55 | console.log(' OR'); 56 | console.log('2. Set the ADMIN_PASSWORD_HASH environment variable in serverless.yml'); 57 | console.log('\nLogin credentials:'); 58 | console.log('Email: YOUR_ADMIN_EMAIL (or the email set in your environment)'); 59 | console.log(`Password: ${newPassword}\n`); 60 | 61 | console.log('=== Remember to deploy your changes! ===\n'); 62 | -------------------------------------------------------------------------------- /backend/src/utils/sms-cost-optimizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fast2SMS Cost Optimizer 3 | * 4 | * Converts Unicode characters to GSM-compatible alternatives 5 | * to optimize SMS costs by avoiding double charges for Unicode 6 | */ 7 | 8 | /** 9 | * Optimize SMS text to avoid Unicode characters that cost extra 10 | * @param {string} message - Original message with potential Unicode characters 11 | * @returns {string} - Optimized message using only GSM characters 12 | */ 13 | function optimizeSmsText(message) { 14 | // Replace common Unicode characters with GSM-compatible alternatives 15 | return message 16 | // Currency symbols 17 | .replace(/₹/g, 'Rs.') 18 | .replace(/€/g, 'EUR') 19 | .replace(/£/g, 'GBP') 20 | .replace(/¥/g, 'JPY') 21 | 22 | // Check marks and crosses 23 | .replace(/✓|✅|☑️|✔️/g, '+') 24 | .replace(/❌|✖️|✘/g, 'X') 25 | 26 | // Bullets and decorative characters 27 | .replace(/•|⁃|‣|⦿|⁌|⁍/g, '-') 28 | .replace(/→|⟶|➔|➜|➞/g, '->') 29 | .replace(/←|⟵|⟸|⬅️/g, '<-') 30 | 31 | // Quotes 32 | .replace(/"|"|❝|❞/g, '"') 33 | .replace(/'|'/g, "'") 34 | 35 | // Dashes and spaces 36 | .replace(/—|–/g, '-') 37 | .replace(/\s+/g, ' ') 38 | 39 | // Emojis (remove completely or replace with text alternatives) 40 | .replace(/😊|😀|😃|😄/g, ':)') 41 | .replace(/😢|😭|😥/g, ':(') 42 | 43 | // General emoji cleanup (remove any remaining emojis) 44 | .replace(/[\u{1F600}-\u{1F64F}]/gu, '') // Face emojis 45 | .replace(/[\u{1F300}-\u{1F5FF}]/gu, '') // Symbols & pictographs 46 | .replace(/[\u{1F680}-\u{1F6FF}]/gu, '') // Transport & map symbols 47 | .replace(/[\u{2600}-\u{26FF}]/gu, '') // Miscellaneous symbols 48 | .replace(/[\u{2700}-\u{27BF}]/gu, '') // Dingbats 49 | .replace(/[\u{1F900}-\u{1F9FF}]/gu, '') // Supplemental symbols and pictographs 50 | .replace(/[\u{1F1E0}-\u{1F1FF}]/gu, ''); // Flags 51 | } 52 | 53 | // Example usage: 54 | /* 55 | const originalMessage = "Your fee of ₹5000 is due on 20th Aug 2023 ✓"; 56 | const optimizedMessage = optimizeSmsText(originalMessage); 57 | console.log("Original:", originalMessage); // Uses Unicode, costs 2 SMS units 58 | console.log("Optimized:", optimizedMessage); // "Your fee of Rs.5000 is due on 20th Aug 2023 +" 59 | */ 60 | 61 | module.exports = { optimizeSmsText }; 62 | -------------------------------------------------------------------------------- /backend/src/utils/auth-middleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | 3 | // JWT secret (should match the one in auth.js) 4 | const JWT_SECRET = process.env.JWT_SECRET || 'YOUR_JWT_SECRET'; 5 | 6 | /** 7 | * Middleware to authenticate JWT tokens 8 | * This will protect routes by verifying the JWT token in the request header 9 | */ 10 | const authMiddleware = (req, res, next) => { 11 | // Get token from header 12 | const authHeader = req.headers['authorization']; 13 | const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN format 14 | 15 | // Check for token in query or cookies as fallbacks (less secure) 16 | const queryToken = req.query.token; 17 | const cookieToken = req.cookies && req.cookies.token; 18 | 19 | // Use the first available token 20 | const finalToken = token || queryToken || cookieToken; 21 | 22 | // If no token is provided through any source 23 | if (!finalToken) { 24 | return res.status(401).json({ 25 | success: false, 26 | message: 'Access denied. Authentication required.' 27 | }); 28 | } 29 | 30 | try { 31 | // Verify the token with stricter options 32 | const decoded = jwt.verify(finalToken, JWT_SECRET, { 33 | algorithms: ['HS256'], // Restrict to specific algorithm 34 | ignoreExpiration: false // Always check expiration 35 | }); 36 | 37 | // Check if token is about to expire (within 30 minutes) 38 | const tokenExp = decoded.exp * 1000; // Convert to milliseconds 39 | const thirtyMinutes = 30 * 60 * 1000; 40 | const isExpiringSoon = tokenExp - Date.now() < thirtyMinutes; 41 | 42 | // Attach user info to request for use in route handlers 43 | req.user = decoded; 44 | req.tokenExpiringSoon = isExpiringSoon; 45 | 46 | // Proceed to the next middleware or route handler 47 | next(); 48 | } catch (error) { 49 | let message = 'Authentication failed.'; 50 | 51 | // Provide more specific messages for common errors 52 | if (error.name === 'TokenExpiredError') { 53 | message = 'Your session has expired. Please log in again.'; 54 | } else if (error.name === 'JsonWebTokenError') { 55 | message = 'Invalid authentication token.'; 56 | } 57 | 58 | return res.status(401).json({ 59 | success: false, 60 | message, 61 | error: error.message 62 | }); 63 | } 64 | }; 65 | 66 | module.exports = authMiddleware; 67 | -------------------------------------------------------------------------------- /backend/tests/direct-sms-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SMS Test Script - Send a test SMS to verify the SMS integration 3 | * 4 | * This script directly uses the Fast2SMS API to send a test message 5 | * It bypasses any layers that might be causing issues in the main application 6 | */ 7 | 8 | // Load environment variables 9 | require('dotenv').config({ path: '.env.local' }); 10 | 11 | const axios = require('axios'); 12 | 13 | async function sendTestSMS() { 14 | // Phone number from our test 15 | const phoneNumber = 'YOUR_TEST_PHONE'; 16 | const message = 'Direct test SMS - Your fee of Rs.5000 is due on 30/07/2025. Please settle your pending dues. Reply STOP to opt out.'; 17 | const apiKey = process.env.FAST2SMS_API_KEY; 18 | 19 | console.log('--- SMS TEST ---'); 20 | console.log(`Phone: ${phoneNumber}`); 21 | console.log(`Message: ${message}`); 22 | console.log(`API Key: ${apiKey ? apiKey.substring(0, 5) + '...' : 'Not set'}`); 23 | 24 | if (!apiKey) { 25 | console.error(' ERROR: FAST2SMS_API_KEY not set in .env.local'); 26 | process.exit(1); 27 | } 28 | 29 | try { 30 | // Send SMS using Fast2SMS API 31 | const response = await axios.get('https://www.fast2sms.com/dev/bulkV2', { 32 | params: { 33 | authorization: apiKey, 34 | route: 'q', // Quick SMS route 35 | message: message, 36 | language: 'english', 37 | flash: '0', 38 | numbers: phoneNumber 39 | } 40 | }); 41 | 42 | console.log('\n--- API RESPONSE ---'); 43 | console.log(JSON.stringify(response.data, null, 2)); 44 | 45 | if (response.data.return === true) { 46 | console.log('\n SUCCESS: SMS sent successfully!'); 47 | console.log('Request ID:', response.data.request_id); 48 | console.log('Message ID:', response.data.message_id); 49 | } else { 50 | console.log('\n ERROR: Fast2SMS API returned an error'); 51 | console.log(response.data); 52 | } 53 | } catch (error) { 54 | console.error('\n ERROR sending SMS:', error.message); 55 | if (error.response) { 56 | console.error('Status:', error.response.status); 57 | console.error('Response:', error.response.data); 58 | } 59 | } 60 | } 61 | 62 | // Run the test 63 | sendTestSMS(); 64 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/tests/create-test-student-prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Test Student with Correct Schema (Live Version) 3 | * 4 | * Creates a student with the exact schema expected by the Lambda function 5 | */ 6 | 7 | // Load environment variables 8 | require('dotenv').config({ path: '.env.local' }); 9 | 10 | const AWS = require('aws-sdk'); 11 | const { v4: uuidv4 } = require('uuid'); 12 | const moment = require('moment'); 13 | 14 | // Configure AWS 15 | AWS.config.update({ 16 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 17 | }); 18 | 19 | const dynamoDB = new AWS.DynamoDB.DocumentClient(); 20 | 21 | // Table name based on serverless.yml 22 | const STUDENT_TABLE = 'Students-pro'; 23 | 24 | // Phone number (default or from command line) 25 | const PHONE_NUMBER = process.argv[2] || 'YOUR_TEST_PHONE'; 26 | const STUDENT_NAME = 'TEST STUDENT'; 27 | 28 | async function createTestStudent() { 29 | console.log('Creating test student with production schema...'); 30 | console.log(`Phone number: ${PHONE_NUMBER}`); 31 | 32 | const tomorrow = moment().add(1, 'days').format('YYYY-MM-DD'); 33 | const studentId = uuidv4(); 34 | 35 | // Create student with schema matching what the Lambda function expects 36 | const student = { 37 | id: studentId, 38 | name: STUDENT_NAME, 39 | phone_no: PHONE_NUMBER, 40 | fathers_name: 'Test Father', 41 | registration_no: `TEST-${Math.floor(Math.random() * 10000)}`, 42 | course_name: 'Test Course', 43 | batch_time: 'Morning', 44 | course_duration: 6, 45 | created_at: new Date().toISOString(), 46 | updated_at: new Date().toISOString(), 47 | fees: { // Notice: fees is an object, not an array 48 | monthly_amount: 1000, 49 | status: 'pending', // lowercase 'pending', not 'PENDING' 50 | due_date: tomorrow 51 | } 52 | }; 53 | 54 | const params = { 55 | TableName: STUDENT_TABLE, 56 | Item: student 57 | }; 58 | 59 | try { 60 | await dynamoDB.put(params).promise(); 61 | console.log(' Test student created successfully:'); 62 | console.log(' ID:', studentId); 63 | console.log(' Name:', STUDENT_NAME); 64 | console.log(' Phone:', PHONE_NUMBER); 65 | console.log(' Fee amount:', 1000); 66 | console.log(' Due date:', tomorrow); 67 | console.log('\n The Lambda function should now send an SMS when it runs!'); 68 | console.log(' (Cron job is scheduled to run every 10 minutes)'); 69 | 70 | return student; 71 | } catch (error) { 72 | console.error(' Error creating test student:', error); 73 | throw error; 74 | } 75 | } 76 | 77 | // Run the function 78 | createTestStudent(); 79 | -------------------------------------------------------------------------------- /backend/src/utils/notification-logs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple Notification Logs Utility 3 | * 4 | * Creates and manages notification logs in DynamoDB 5 | */ 6 | 7 | const AWS = require('aws-sdk'); 8 | const { v4: uuidv4 } = require('uuid'); 9 | 10 | AWS.config.update({ 11 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 12 | }); 13 | 14 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 15 | const TABLE_NAME = process.env.NOTIFICATION_LOGS_TABLE || 'NotificationLogs-pro'; 16 | 17 | const NotificationLog = { 18 | async create(notificationData) { 19 | // Add missing fields if not provided 20 | const log = { 21 | id: uuidv4(), 22 | student_id: notificationData.studentId || null, 23 | student_name: notificationData.studentName || null, 24 | phone_number: notificationData.phoneNumber || null, 25 | message: notificationData.message || null, 26 | status: notificationData.status || 'sent', 27 | type: notificationData.type || 'sms', 28 | message_id: notificationData.messageId || null, 29 | provider: notificationData.provider || 'fast2sms', 30 | template_name: notificationData.templateName || null, 31 | created_at: new Date().toISOString(), 32 | metadata: notificationData.metadata || {}, 33 | response_data: notificationData.responseData || {} 34 | }; 35 | 36 | const params = { 37 | TableName: TABLE_NAME, 38 | Item: log 39 | }; 40 | 41 | try { 42 | await dynamodb.put(params).promise(); 43 | return log; 44 | } catch (error) { 45 | console.error('Error creating notification log:', error); 46 | // Don't throw - this is just a logging function and should not disrupt main flow 47 | return null; 48 | } 49 | }, 50 | 51 | async getByStudentId(studentId) { 52 | const params = { 53 | TableName: TABLE_NAME, 54 | IndexName: 'StudentIdIndex', 55 | KeyConditionExpression: 'student_id = :studentId', 56 | ExpressionAttributeValues: { 57 | ':studentId': studentId 58 | }, 59 | ScanIndexForward: false // Sort by created_at in descending order (newest first) 60 | }; 61 | 62 | try { 63 | const result = await dynamodb.query(params).promise(); 64 | return result.Items; 65 | } catch (error) { 66 | console.error('Error retrieving notification logs:', error); 67 | return []; 68 | } 69 | } 70 | }; 71 | 72 | module.exports = NotificationLog; 73 | -------------------------------------------------------------------------------- /backend/tests/create-test-student-fixed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Test Student (Schema Compatible) 3 | * 4 | * This script creates a test student with the correct schema for the production system. 5 | */ 6 | 7 | // Load environment variables 8 | require('dotenv').config({ path: '.env.local' }); 9 | 10 | const AWS = require('aws-sdk'); 11 | const { v4: uuidv4 } = require('uuid'); 12 | const moment = require('moment'); 13 | 14 | // Configure AWS 15 | AWS.config.update({ 16 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 17 | }); 18 | 19 | const dynamoDB = new AWS.DynamoDB.DocumentClient(); 20 | 21 | // Table name 22 | const STUDENT_TABLE = process.env.STUDENTS_TABLE || 'Students-pro'; 23 | 24 | // Phone number (default or from command line) 25 | const PHONE_NUMBER = process.argv[2] || 'YOUR_TEST_PHONE'; 26 | const STUDENT_NAME = 'TEST STUDENT'; 27 | 28 | async function createTestStudentCorrectSchema() { 29 | console.log('Creating test student with correct schema...'); 30 | console.log(`Phone number: ${PHONE_NUMBER}`); 31 | 32 | const tomorrow = moment().add(1, 'days').format('YYYY-MM-DD'); 33 | const studentId = uuidv4(); 34 | 35 | // Create student with schema matching what the Lambda function expects 36 | const student = { 37 | id: studentId, 38 | name: STUDENT_NAME, 39 | phone_no: PHONE_NUMBER, // Notice: phone_no not phone 40 | registration_no: `TEST-${Math.floor(Math.random() * 10000)}`, 41 | fathers_name: 'Test Father', 42 | course_name: 'Test Course', 43 | batch_time: 'Morning', 44 | course_duration: 6, 45 | created_at: new Date().toISOString(), 46 | updated_at: new Date().toISOString(), 47 | fees: { // Notice: fees is an object, not an array 48 | monthly_amount: 1000, 49 | status: 'pending', // lowercase 'pending', not 'PENDING' 50 | due_date: tomorrow 51 | } 52 | }; 53 | 54 | const params = { 55 | TableName: STUDENT_TABLE, 56 | Item: student 57 | }; 58 | 59 | try { 60 | await dynamoDB.put(params).promise(); 61 | console.log(' Test student created successfully:'); 62 | console.log(' ID:', studentId); 63 | console.log(' Name:', STUDENT_NAME); 64 | console.log(' Phone:', PHONE_NUMBER); 65 | console.log(' Fee amount:', 1000); 66 | console.log(' Due date:', tomorrow); 67 | console.log('\n The next scheduled fee reminder should send an SMS to this number!'); 68 | console.log(' (According to the scheduled cron job running every 10 minutes)'); 69 | 70 | return student; 71 | } catch (error) { 72 | console.error(' Error creating test student:', error); 73 | throw error; 74 | } 75 | } 76 | 77 | // Run the function 78 | createTestStudentCorrectSchema(); 79 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/components/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | // Create file at: src/components/ProtectedRoute.js 2 | import React, { useState, useEffect } from 'react'; 3 | import { Navigate } from 'react-router-dom'; 4 | import { API_BASE_URL } from '../config'; 5 | 6 | const ProtectedRoute = ({ children }) => { 7 | const [isVerifying, setIsVerifying] = useState(true); 8 | const [isAuthenticated, setIsAuthenticated] = useState(false); 9 | 10 | useEffect(() => { 11 | const verifyToken = async () => { 12 | const token = localStorage.getItem('authToken'); 13 | const expiryTime = localStorage.getItem('authTokenExpiry'); 14 | 15 | // Check if token exists 16 | if (!token) { 17 | setIsVerifying(false); 18 | return; 19 | } 20 | 21 | // Check if token has expired based on local expiry time 22 | if (expiryTime && new Date(expiryTime) <= new Date()) { 23 | // Token has expired locally 24 | localStorage.removeItem('authToken'); 25 | localStorage.removeItem('user'); 26 | localStorage.removeItem('authTokenExpiry'); 27 | setIsVerifying(false); 28 | return; 29 | } 30 | 31 | try { 32 | // Verify token with backend 33 | const response = await fetch(`${API_BASE_URL}/api/auth/verify`, { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json' 37 | }, 38 | body: JSON.stringify({ token }) 39 | }); 40 | 41 | const data = await response.json(); 42 | 43 | if (data.success) { 44 | setIsAuthenticated(true); 45 | } else { 46 | // Token is invalid according to backend, clear storage 47 | localStorage.removeItem('authToken'); 48 | localStorage.removeItem('user'); 49 | localStorage.removeItem('authTokenExpiry'); 50 | } 51 | } catch (error) { 52 | console.error('Token verification error:', error); 53 | // On network errors, we'll assume token is valid if it exists 54 | // This allows the app to work offline if needed 55 | setIsAuthenticated(true); 56 | } 57 | 58 | setIsVerifying(false); 59 | }; 60 | 61 | verifyToken(); 62 | }, []); 63 | 64 | // Show loading state while verifying 65 | if (isVerifying) { 66 | return
67 |
68 | Loading... 69 |
70 |
; 71 | } 72 | 73 | // Redirect to login if not authenticated 74 | if (!isAuthenticated) { 75 | return ; 76 | } 77 | 78 | // If authenticated, render the protected component 79 | return children; 80 | }; 81 | 82 | export default ProtectedRoute; -------------------------------------------------------------------------------- /backend/utils/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Local development server for the fee management system backend 3 | */ 4 | // Load environment variables from .env.local file 5 | const dotenv = require('dotenv'); 6 | const path = require('path'); 7 | dotenv.config({ path: path.join(__dirname, '.env.local') }); 8 | 9 | // Dynamically import the Express app with backward compatibility 10 | let app; 11 | try { 12 | // Try to import the app using the app export 13 | const appModule = require('./src/app'); 14 | app = appModule.app; 15 | if (!app) { 16 | // Fallback if app is not exported but handler is 17 | console.warn('App export not found, creating Express instance from handler'); 18 | const express = require('express'); 19 | app = express(); 20 | // Add basic routes if we had to create a new app 21 | app.get('/health', (req, res) => { 22 | res.json({ status: 'OK', timestamp: new Date().toISOString() }); 23 | }); 24 | } 25 | } catch (error) { 26 | console.error('Error importing app:', error); 27 | process.exit(1); 28 | } 29 | 30 | // Set up mock environment variables for local development if not already set 31 | process.env.STUDENTS_TABLE = process.env.STUDENTS_TABLE || 'Students-local'; 32 | process.env.FEES_TABLE = process.env.FEES_TABLE || 'Fees-local'; 33 | process.env.NOTIFICATION_LOGS_TABLE = process.env.NOTIFICATION_LOGS_TABLE || 'NotificationLogs-local'; 34 | process.env.JWT_SECRET = process.env.JWT_SECRET || 'YOUR_JWT_SECRET'; 35 | process.env.SMS_ENABLED = process.env.SMS_ENABLED || 'true'; 36 | process.env.FAST2SMS_API_KEY = process.env.FAST2SMS_API_KEY || 'YOUR_FAST2SMS_API_KEY'; 37 | 38 | // Add test endpoint for SMS testing 39 | app.get('/test', async (req, res) => { 40 | try { 41 | const { sendCustomSMS } = require('./src/utils/notification-v2'); 42 | const phoneNumber = 'YOUR_TEST_PHONE'; // Phone number from check-students.js 43 | const message = "Test message from local development server"; 44 | const metadata = { studentId: "test-student", messageType: "test" }; 45 | 46 | console.log(`TEST ENDPOINT: Sending test SMS to ${phoneNumber}`); 47 | const result = await sendCustomSMS(phoneNumber, message, metadata); 48 | 49 | return res.json({ 50 | success: true, 51 | message: 'Test SMS sent!', 52 | result 53 | }); 54 | } catch (error) { 55 | console.error('TEST ENDPOINT ERROR:', error); 56 | return res.status(500).json({ 57 | success: false, 58 | error: error.message 59 | }); 60 | } 61 | }); 62 | 63 | // Start the local server 64 | const PORT = process.env.PORT || 3000; 65 | app.listen(PORT, () => { 66 | console.log(` 67 | Fee Management System Backend Server 68 | ===================================== 69 | Local server running at http://localhost:${PORT} 70 | Try the health endpoint: http://localhost:${PORT}/health 71 | Test SMS endpoint: http://localhost:${PORT}/test 72 | Admin login: http://localhost:${PORT}/api/auth/login 73 | 74 | Environment: ${process.env.NODE_ENV || 'development'} 75 | `); 76 | }); 77 | -------------------------------------------------------------------------------- /backend/tests/create-test-student.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create Test Student for SMS Reminder Testing 3 | * 4 | * This script adds a test student with the specified phone number 5 | * and sets a due date that will trigger the reminder. 6 | */ 7 | 8 | // Load environment variables 9 | require('dotenv').config({ path: '.env.local' }); 10 | 11 | const AWS = require('aws-sdk'); 12 | const { v4: uuidv4 } = require('uuid'); 13 | 14 | // Configure AWS 15 | AWS.config.update({ 16 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 17 | }); 18 | 19 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 20 | const STUDENTS_TABLE = process.env.STUDENTS_TABLE || 'Students-local'; 21 | const FEES_TABLE = process.env.FEES_TABLE || 'Fees-local'; 22 | 23 | async function createTestStudent() { 24 | const studentId = uuidv4(); 25 | const phoneNumber = 'YOUR_TEST_PHONE'; // Your phone number 26 | 27 | // Set due date to tomorrow for testing 28 | const today = new Date(); 29 | const tomorrow = new Date(today); 30 | tomorrow.setDate(today.getDate() + 1); 31 | const dueDate = tomorrow.toISOString().split('T')[0]; // Format: YYYY-MM-DD 32 | 33 | console.log('Creating test student with due date:', dueDate); 34 | 35 | const studentParams = { 36 | TableName: STUDENTS_TABLE, 37 | Item: { 38 | id: studentId, 39 | name: 'Test Student (SMS)', 40 | phone_no: phoneNumber, 41 | email: 'test@example.com', 42 | registration_no: `TEST${Math.floor(100000 + Math.random() * 900000)}`, 43 | course_name: 'Test SMS Course', 44 | status: 'active', 45 | created_at: new Date().toISOString() 46 | } 47 | }; 48 | 49 | const feeParams = { 50 | TableName: FEES_TABLE, 51 | Item: { 52 | id: uuidv4(), 53 | student_id: studentId, 54 | monthly_amount: 5000, 55 | due_date: dueDate, 56 | status: 'pending', 57 | description: 'Test Fee for SMS Reminder', 58 | created_at: new Date().toISOString() 59 | } 60 | }; 61 | 62 | try { 63 | // Create student record 64 | console.log('Creating student record...'); 65 | await dynamodb.put(studentParams).promise(); 66 | console.log('Student record created!'); 67 | 68 | // Create fee record 69 | console.log('Creating fee record...'); 70 | await dynamodb.put(feeParams).promise(); 71 | console.log('Fee record created!'); 72 | 73 | console.log('\n SUCCESS: Test student created with the following details:'); 74 | console.log('- Student ID:', studentId); 75 | console.log('- Name: Test Student (SMS)'); 76 | console.log('- Phone:', phoneNumber); 77 | console.log('- Due Date:', dueDate); 78 | console.log('- Amount:', 5000); 79 | 80 | return { 81 | success: true, 82 | studentId, 83 | phoneNumber, 84 | dueDate 85 | }; 86 | } catch (error) { 87 | console.error(' ERROR creating test student:', error); 88 | return { 89 | success: false, 90 | error: error.message 91 | }; 92 | } 93 | } 94 | 95 | // Run the function 96 | createTestStudent(); 97 | -------------------------------------------------------------------------------- /backend/tests/check-endpoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple API Endpoint Check 3 | */ 4 | const https = require('https'); 5 | 6 | // API Base URL 7 | const API_BASE = 'YOUR_API_ENDPOINT'; 8 | 9 | // Endpoints to check 10 | const endpoints = [ 11 | { path: '/test', method: 'GET', name: 'SMS Test Endpoint' }, 12 | { path: '/health', method: 'GET', name: 'Health Check' }, 13 | { path: '/api/auth/login', method: 'POST', name: 'Authentication', body: { email: 'YOUR_ADMIN_EMAIL', password: 'YOUR_ADMIN_PASSWORD' } } 14 | ]; 15 | 16 | // Function to make HTTP request 17 | function makeRequest(endpoint) { 18 | return new Promise((resolve) => { 19 | console.log(`\nTesting ${endpoint.name}: ${endpoint.method} ${API_BASE}${endpoint.path}`); 20 | 21 | const options = { 22 | hostname: API_BASE.replace('https://', '').split('/')[0], 23 | path: '/pro' + endpoint.path, 24 | method: endpoint.method, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | timeout: 5000, 29 | }; 30 | 31 | const req = https.request(options, (res) => { 32 | let data = ''; 33 | 34 | res.on('data', (chunk) => { 35 | data += chunk; 36 | }); 37 | 38 | res.on('end', () => { 39 | console.log(`Status: ${res.statusCode}`); 40 | try { 41 | const jsonData = JSON.parse(data); 42 | console.log('Response:', JSON.stringify(jsonData, null, 2)); 43 | resolve({ success: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data: jsonData }); 44 | } catch (e) { 45 | console.log('Response (raw):', data); 46 | resolve({ success: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data }); 47 | } 48 | }); 49 | }); 50 | 51 | req.on('error', (error) => { 52 | console.error(`Error with ${endpoint.name}:`, error.message); 53 | resolve({ success: false, error: error.message }); 54 | }); 55 | 56 | req.on('timeout', () => { 57 | console.error(`Timeout with ${endpoint.name}`); 58 | req.destroy(); 59 | resolve({ success: false, error: 'Timeout' }); 60 | }); 61 | 62 | if (endpoint.body) { 63 | req.write(JSON.stringify(endpoint.body)); 64 | } 65 | 66 | req.end(); 67 | }); 68 | } 69 | 70 | // Main function to test all endpoints 71 | async function testEndpoints() { 72 | console.log(' API ENDPOINT CHECK'); 73 | console.log('===================='); 74 | console.log(`API Base: ${API_BASE}`); 75 | 76 | let passed = 0; 77 | let failed = 0; 78 | 79 | for (const endpoint of endpoints) { 80 | const result = await makeRequest(endpoint); 81 | if (result.success) { 82 | console.log(` ${endpoint.name}: PASSED`); 83 | passed++; 84 | } else { 85 | console.log(` ${endpoint.name}: FAILED`); 86 | failed++; 87 | } 88 | } 89 | 90 | console.log('\n=== SUMMARY ==='); 91 | console.log(`Total: ${endpoints.length}`); 92 | console.log(`Passed: ${passed}`); 93 | console.log(`Failed: ${failed}`); 94 | } 95 | 96 | // Run tests 97 | testEndpoints().catch(console.error); 98 | -------------------------------------------------------------------------------- /backend/src/utils/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Legacy compatibility layer for notification.js 3 | * 4 | * This file redirects legacy notification functions to the new notification-v2.js implementation 5 | * It ensures backward compatibility with code that imports from the old file 6 | */ 7 | const { 8 | sendSMSNotification, 9 | sendFeeReminder, 10 | sendFamilyReminder, 11 | sendBulkFeeReminders, 12 | sendCustomSMS 13 | } = require('./notification-v2'); 14 | 15 | /** 16 | * Legacy notifyStudent function - redirects to the new sendCustomSMS function 17 | * 18 | * @param {string} phoneNumber - Student phone number 19 | * @param {string} studentName - Student name 20 | * @param {number} amount - Fee amount 21 | * @param {string} courseName - Course name 22 | * @param {string} dueDate - Due date 23 | * @returns {Promise} Result of sending notification 24 | */ 25 | async function notifyStudent(phoneNumber, studentName, amount, courseName, dueDate) { 26 | console.log('Legacy notifyStudent function called - redirecting to sendCustomSMS'); 27 | 28 | // Create a formatted message 29 | const message = `Fee Reminder: Hi ${studentName}, your fee of Rs.${amount} for ${courseName} is due on ${dueDate}. Please settle your pending dues. Reply STOP to opt out.`; 30 | 31 | // Send using the new function 32 | return sendCustomSMS(phoneNumber, message, { 33 | studentName, 34 | amount, 35 | courseName, 36 | dueDate, 37 | type: 'fee_reminder' 38 | }); 39 | } 40 | 41 | /** 42 | * Legacy notifyOwner function - redirects to sendCustomSMS 43 | * The legacy function expected (ownerPhone, message, dueDate, totalAmount) parameters 44 | */ 45 | async function notifyOwner(ownerPhone, message, dueDate, totalAmount) { 46 | console.log('Legacy notifyOwner function called - redirecting to sendCustomSMS'); 47 | // Create a proper formatted message instead of sending raw message 48 | const formattedMessage = `Admin Notification: ${message} have fees due on ${dueDate || 'upcoming days'}. Total pending amount: Rs.${totalAmount || 0}`; 49 | return sendCustomSMS(ownerPhone, formattedMessage, {type: 'owner_notification', totalAmount, dueDate}); 50 | } 51 | 52 | /** 53 | * Legacy notifyPaymentConfirmation function 54 | */ 55 | async function notifyPaymentConfirmation(phoneNumber, studentName, amount, courseName, receiptId) { 56 | console.log('Legacy notifyPaymentConfirmation called - redirecting to sendCustomSMS'); 57 | 58 | // Create a formatted payment confirmation message 59 | const message = `Payment Confirmation: Payment of Rs.${amount} received for ${studentName}'s ${courseName} course. Receipt ID: ${receiptId}. Thank you!`; 60 | 61 | // Send using the new function 62 | return sendCustomSMS(phoneNumber, message, { 63 | studentName, 64 | amount, 65 | courseName, 66 | receiptId, 67 | type: 'payment_confirmation' 68 | }); 69 | } 70 | 71 | module.exports = { 72 | notifyStudent, 73 | notifyOwner, 74 | notifyPaymentConfirmation, 75 | // Also export the new functions for convenience 76 | sendCustomSMS, 77 | sendSMSNotification, 78 | sendFeeReminder 79 | }; 80 | -------------------------------------------------------------------------------- /backend/tests/check-reminder-eligibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check Reminder Eligibility 3 | * 4 | * This script scans the Students table and reports which students 5 | * are eligible for fee reminders based on tomorrow's due dates. 6 | * Use this to confirm that your test student will be included in 7 | * the automated reminders. 8 | */ 9 | 10 | // Load environment variables 11 | require('dotenv').config({ path: '.env.local' }); 12 | 13 | const AWS = require('aws-sdk'); 14 | const moment = require('moment'); 15 | 16 | // Configure AWS 17 | AWS.config.update({ 18 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 19 | }); 20 | 21 | const dynamoDB = new AWS.DynamoDB.DocumentClient(); 22 | 23 | // Table name 24 | const STUDENT_TABLE = 'Students-pro'; 25 | 26 | async function checkReminders() { 27 | console.log(' Checking for students with fees due tomorrow...'); 28 | 29 | const tomorrow = moment().add(1, 'days').format('YYYY-MM-DD'); 30 | console.log(` Looking for due date: ${tomorrow}`); 31 | 32 | try { 33 | // Scan the table to find all students 34 | const result = await dynamoDB.scan({ 35 | TableName: STUDENT_TABLE 36 | }).promise(); 37 | 38 | if (!result.Items || result.Items.length === 0) { 39 | console.log(' No student records found!'); 40 | return; 41 | } 42 | 43 | console.log(` Found ${result.Items.length} total student records`); 44 | 45 | // Filter for students with fees due tomorrow 46 | const studentsWithDueFees = result.Items.filter(student => { 47 | if (student.recordType !== 'STUDENT' || !student.fees) return false; 48 | 49 | // Check if any fee is due tomorrow 50 | return student.fees.some(fee => 51 | fee.dueDate === tomorrow && fee.status === 'PENDING' 52 | ); 53 | }); 54 | 55 | console.log(`\n Results: ${studentsWithDueFees.length} students have fees due tomorrow`); 56 | 57 | if (studentsWithDueFees.length === 0) { 58 | console.log(' No students found with fees due tomorrow!'); 59 | console.log(' Double-check that the test student was created with the correct due date.'); 60 | return; 61 | } 62 | 63 | // Display the students eligible for reminders 64 | console.log('\n Students eligible for fee reminders:'); 65 | studentsWithDueFees.forEach((student, index) => { 66 | console.log(`\n${index + 1}. Student ID: ${student.id}`); 67 | console.log(` Name: ${student.name}`); 68 | console.log(` Phone: +91${student.phone}`); 69 | 70 | // Find and display due fees 71 | const dueFees = student.fees.filter(fee => 72 | fee.dueDate === tomorrow && fee.status === 'PENDING' 73 | ); 74 | 75 | console.log(` Due Fees: ${dueFees.length}`); 76 | dueFees.forEach((fee, i) => { 77 | console.log(` - Fee #${i + 1}: Rs. ${fee.amount} (Due: ${fee.dueDate})`); 78 | }); 79 | }); 80 | 81 | console.log('\n These students should receive SMS reminders when the cron job runs!'); 82 | console.log(' The cron job should run every 10 minutes as configured.'); 83 | 84 | } catch (error) { 85 | console.error(' Error checking reminders:', error); 86 | } 87 | } 88 | 89 | // Run the check 90 | checkReminders(); 91 | -------------------------------------------------------------------------------- /backend/utils/local-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Local Development Server for Testing Endpoints 3 | * 4 | * This script runs the API locally to test endpoints before deploying to production. 5 | */ 6 | 7 | // Import dependencies 8 | const express = require('express'); 9 | const app = require('./src/app').app; // Importing the Express app without serverless wrapper 10 | 11 | // Set environment variables for local testing 12 | process.env.NODE_ENV = 'development'; 13 | process.env.SMS_ENABLED = 'true'; 14 | process.env.NOTIFICATION_PROVIDER = 'fast2sms'; 15 | 16 | // Configure port 17 | const PORT = process.env.PORT || 3000; 18 | 19 | // Add a test endpoint for local testing 20 | app.get('/test', async (req, res) => { 21 | try { 22 | console.log('Local TEST endpoint called'); 23 | 24 | // Get notification utility 25 | const { sendSMSNotification } = require('./src/utils/notification-v2'); 26 | 27 | // Send test message to the same number used in production 28 | const result = await sendSMSNotification( 29 | "YOUR_TEST_PHONE", 30 | "Test message from LOCAL SERVER", 31 | { studentId: "test-student", messageType: "local-test" } 32 | ); 33 | 34 | return res.json({ 35 | success: true, 36 | message: 'Local test SMS sent!', 37 | result, 38 | environment: 'local' 39 | }); 40 | } catch (error) { 41 | console.error('LOCAL TEST ENDPOINT ERROR:', error); 42 | return res.status(500).json({ 43 | success: false, 44 | error: error.message 45 | }); 46 | } 47 | }); 48 | 49 | // Add a debug endpoint to test SMS cost optimization 50 | app.get('/debug/sms-template', (req, res) => { 51 | const { optimizeSmsText } = require('./src/utils/sms-cost-optimizer'); 52 | 53 | // Original message with Unicode character 54 | const originalMessage = "Your fee of ₹5000 is due on 20th Aug 2025 ✓"; 55 | 56 | // Optimized message 57 | const optimizedMessage = optimizeSmsText(originalMessage); 58 | 59 | res.json({ 60 | original: { 61 | message: originalMessage, 62 | length: originalMessage.length, 63 | cost: "₹10 (2 SMS units)", 64 | reason: "Contains Unicode characters (₹, ✓) and exceeds 70 chars" 65 | }, 66 | optimized: { 67 | message: optimizedMessage, 68 | length: optimizedMessage.length, 69 | cost: "₹5 (1 SMS unit)", 70 | reason: "Uses only GSM-7 characters and fits in 160 chars" 71 | } 72 | }); 73 | }); 74 | 75 | // Start the server 76 | app.listen(PORT, () => { 77 | console.log(` 78 | LOCAL TEST SERVER STARTED 79 | 80 | Server running at: http://localhost:${PORT} 81 | Environment: ${process.env.NODE_ENV} 82 | 83 | Available Test Endpoints: 84 | - Health Check: http://localhost:${PORT}/health 85 | - SMS Test: http://localhost:${PORT}/test 86 | - SMS Template Debug: http://localhost:${PORT}/debug/sms-template 87 | - Auth Login: http://localhost:${PORT}/api/auth/login (POST) 88 | 89 | SMS Configuration: 90 | - Provider: ${process.env.NOTIFICATION_PROVIDER} 91 | - Enabled: ${process.env.SMS_ENABLED} 92 | - Cost Optimization: ENABLED (Rs. instead of ₹) 93 | 94 | Press Ctrl+C to stop the server. 95 | `); 96 | }); 97 | -------------------------------------------------------------------------------- /backend/tests/test-sms-direct.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Direct SMS API Test 3 | * 4 | * This script tests the Fast2SMS API directly without going through the Lambda function. 5 | * It helps identify if there are issues with the API key or configuration. 6 | */ 7 | 8 | // Load environment variables 9 | require('dotenv').config({ path: '.env.local' }); 10 | 11 | const axios = require('axios'); 12 | 13 | // Constants 14 | const FAST2SMS_API_KEY = process.env.FAST2SMS_API_KEY; 15 | const PHONE_NUMBER = process.argv[2] || 'YOUR_TEST_PHONE'; 16 | 17 | if (!FAST2SMS_API_KEY) { 18 | console.error(' ERROR: FAST2SMS_API_KEY environment variable is not set!'); 19 | console.log(' Please check your .env.local file or set the variable.'); 20 | process.exit(1); 21 | } 22 | 23 | async function testSMS() { 24 | console.log(' Testing Fast2SMS API directly'); 25 | console.log(` Sending test SMS to: +91${PHONE_NUMBER}`); 26 | console.log(` API Key present: ${FAST2SMS_API_KEY ? 'YES' : 'NO'}`); 27 | 28 | // Print the masked API key for verification 29 | const maskedKey = FAST2SMS_API_KEY.substring(0, 4) + '...' + 30 | FAST2SMS_API_KEY.substring(FAST2SMS_API_KEY.length - 4); 31 | console.log(` Key starts with: ${maskedKey}`); 32 | 33 | // Prepare the message 34 | const message = `TEST MSG: This is a test SMS sent at ${new Date().toLocaleTimeString()} to verify the Fast2SMS integration.`; 35 | 36 | try { 37 | // Log the API request details 38 | console.log('\n Sending API request:'); 39 | console.log(` URL: https://www.fast2sms.com/dev/bulkV2`); 40 | console.log(` Method: GET`); 41 | console.log(` Message: ${message}`); 42 | 43 | // Make the API request - using GET method as required by Fast2SMS 44 | const response = await axios({ 45 | method: 'get', 46 | url: 'https://www.fast2sms.com/dev/bulkV2', 47 | params: { 48 | authorization: FAST2SMS_API_KEY, 49 | route: 'v3', 50 | sender_id: 'TXTIND', 51 | message: message, 52 | language: 'english', 53 | flash: 0, 54 | numbers: PHONE_NUMBER, 55 | } 56 | }); 57 | 58 | // Log the API response 59 | console.log('\n API Response:'); 60 | console.log(JSON.stringify(response.data, null, 2)); 61 | 62 | if (response.data && response.data.return === true) { 63 | console.log('\n SUCCESS: SMS sent successfully!'); 64 | console.log(` Message ID: ${response.data.message_id}`); 65 | console.log(` Request ID: ${response.data.request_id}`); 66 | } else { 67 | console.log('\n FAILURE: SMS sending failed!'); 68 | console.log(` Message: ${response.data.message}`); 69 | } 70 | 71 | } catch (error) { 72 | console.error('\n ERROR making API request:'); 73 | 74 | if (error.response) { 75 | // The request was made and the server responded with a status code 76 | // that falls out of the range of 2xx 77 | console.error(' Status:', error.response.status); 78 | console.error(' Data:', JSON.stringify(error.response.data, null, 2)); 79 | } else if (error.request) { 80 | // The request was made but no response was received 81 | console.error(' No response received from API'); 82 | console.error(error.request); 83 | } else { 84 | // Something happened in setting up the request that triggered an Error 85 | console.error(' Error message:', error.message); 86 | } 87 | } 88 | } 89 | 90 | // Run the test 91 | testSMS(); 92 | -------------------------------------------------------------------------------- /backend/src/utils/simple-logs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Notification Logs Utility - Simple File-based Logging 3 | * Tracks sent notifications for analytics and auditing 4 | */ 5 | const fs = require('fs').promises; 6 | const path = require('path'); 7 | 8 | // Create logs directory if it doesn't exist 9 | const LOGS_DIR = path.join(__dirname, '../../logs'); 10 | const LOGS_FILE = path.join(LOGS_DIR, 'notifications.jsonl'); 11 | 12 | const NotificationLog = { 13 | /** 14 | * Record a notification in the log file 15 | * @param {object} notification - The notification data to record 16 | * @returns {Promise} The logged notification 17 | */ 18 | async create(notification) { 19 | try { 20 | // Ensure logs directory exists 21 | await fs.mkdir(LOGS_DIR, { recursive: true }); 22 | 23 | // Create log entry with timestamp 24 | const logEntry = { 25 | id: Date.now().toString(), 26 | timestamp: new Date().toISOString(), 27 | phoneNumber: notification.phoneNumber, 28 | studentId: notification.studentId, 29 | studentName: notification.studentName, 30 | type: notification.type || 'sms', 31 | message: notification.message, 32 | status: notification.status, 33 | messageId: notification.messageId, 34 | provider: notification.provider || 'fast2sms', 35 | responseData: notification.responseData || {}, 36 | metadata: notification.metadata || {} 37 | }; 38 | 39 | // Append to log file (JSONL format - one JSON object per line) 40 | await fs.appendFile(LOGS_FILE, JSON.stringify(logEntry) + '\n'); 41 | 42 | console.log(`Notification logged: ${logEntry.id}`); 43 | return logEntry; 44 | } catch (error) { 45 | console.error('Error logging notification:', error); 46 | // Don't fail the SMS sending if logging fails 47 | return { error: error.message }; 48 | } 49 | }, 50 | 51 | /** 52 | * Get recent notifications from log file 53 | * @param {number} limit - Number of recent notifications to retrieve 54 | * @returns {Promise} Array of recent notifications 55 | */ 56 | async getRecent(limit = 50) { 57 | try { 58 | const data = await fs.readFile(LOGS_FILE, 'utf8'); 59 | const lines = data.trim().split('\n').filter(line => line); 60 | 61 | // Get last N lines and parse them 62 | const recentLines = lines.slice(-limit); 63 | return recentLines.map(line => { 64 | try { 65 | return JSON.parse(line); 66 | } catch (e) { 67 | return null; 68 | } 69 | }).filter(Boolean); 70 | } catch (error) { 71 | if (error.code === 'ENOENT') { 72 | return []; // File doesn't exist yet 73 | } 74 | console.error('Error reading notification logs:', error); 75 | return []; 76 | } 77 | }, 78 | 79 | /** 80 | * Get daily statistics 81 | * @returns {Promise} Daily SMS statistics 82 | */ 83 | async getDailyStats() { 84 | try { 85 | const today = new Date().toISOString().split('T')[0]; 86 | const allLogs = await this.getRecent(500); 87 | 88 | const todayLogs = allLogs.filter(log => 89 | log.timestamp && log.timestamp.startsWith(today) 90 | ); 91 | 92 | const stats = { 93 | date: today, 94 | total: todayLogs.length, 95 | successful: todayLogs.filter(log => log.status === 'sent').length, 96 | failed: todayLogs.filter(log => log.status === 'failed').length, 97 | cost: todayLogs.filter(log => log.status === 'sent').length * 5 // ₹5 per SMS 98 | }; 99 | 100 | return stats; 101 | } catch (error) { 102 | console.error('Error calculating daily stats:', error); 103 | return { date: new Date().toISOString().split('T')[0], total: 0, successful: 0, failed: 0, cost: 0 }; 104 | } 105 | } 106 | }; 107 | 108 | module.exports = NotificationLog; 109 | -------------------------------------------------------------------------------- /backend/tests/test-sms-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fast2SMS Test Server 3 | * 4 | * A simplified Express server for testing Fast2SMS integration 5 | */ 6 | 7 | // Load environment variables 8 | require('dotenv').config({ path: '.env.local' }); 9 | 10 | const express = require('express'); 11 | const axios = require('axios'); 12 | const app = express(); 13 | 14 | // Middleware 15 | app.use(express.json()); 16 | app.use(express.urlencoded({ extended: true })); 17 | 18 | // Enable CORS 19 | app.use((req, res, next) => { 20 | res.header('Access-Control-Allow-Origin', '*'); 21 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 22 | res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); 23 | if (req.method === 'OPTIONS') { 24 | return res.sendStatus(200); 25 | } 26 | next(); 27 | }); 28 | 29 | // Health check endpoint 30 | app.get('/health', (req, res) => { 31 | res.json({ 32 | status: 'OK', 33 | timestamp: new Date().toISOString(), 34 | message: 'Fast2SMS Test Server is running' 35 | }); 36 | }); 37 | 38 | /** 39 | * Direct Fast2SMS test endpoint 40 | * 41 | * Directly calls the Fast2SMS API without any intermediate layers 42 | * to identify issues with the integration 43 | */ 44 | app.get('/direct-test', async (req, res) => { 45 | try { 46 | const apiKey = process.env.FAST2SMS_API_KEY; 47 | const phoneNumber = 'YOUR_TEST_PHONE'; 48 | const message = 'Direct test of Fast2SMS API with GSM-compatible message using Rs. instead of ₹ symbol'; 49 | 50 | console.log(`Sending direct SMS test to ${phoneNumber}`); 51 | console.log(`API Key: ${apiKey ? apiKey.substring(0, 5) + '...' : 'Not set'}`); 52 | 53 | if (!apiKey) { 54 | return res.status(400).json({ 55 | success: false, 56 | error: 'FAST2SMS_API_KEY not set in .env.local' 57 | }); 58 | } 59 | 60 | // Make direct API call to Fast2SMS 61 | const response = await axios.get('https://www.fast2sms.com/dev/bulkV2', { 62 | params: { 63 | authorization: apiKey, 64 | route: 'q', 65 | message: message, 66 | language: 'english', 67 | flash: '0', 68 | numbers: phoneNumber 69 | } 70 | }); 71 | 72 | console.log('Fast2SMS Response:', JSON.stringify(response.data)); 73 | 74 | return res.json({ 75 | success: true, 76 | message: 'Direct Fast2SMS test', 77 | request: { phoneNumber, messagePreview: message }, 78 | response: response.data 79 | }); 80 | 81 | } catch (error) { 82 | console.error('DIRECT TEST ERROR:', error); 83 | 84 | // Log more information about the error 85 | let errorDetails = { message: error.message }; 86 | if (error.response) { 87 | errorDetails.status = error.response.status; 88 | errorDetails.data = error.response.data; 89 | } 90 | 91 | return res.status(500).json({ 92 | success: false, 93 | error: errorDetails 94 | }); 95 | } 96 | }); 97 | 98 | /** 99 | * Get wallet balance from Fast2SMS 100 | */ 101 | app.get('/wallet-balance', async (req, res) => { 102 | try { 103 | const apiKey = process.env.FAST2SMS_API_KEY; 104 | 105 | if (!apiKey) { 106 | return res.status(400).json({ 107 | success: false, 108 | error: 'FAST2SMS_API_KEY not set in .env.local' 109 | }); 110 | } 111 | 112 | // Check wallet balance 113 | const response = await axios.get('https://www.fast2sms.com/dev/wallet', { 114 | params: { 115 | authorization: apiKey 116 | } 117 | }); 118 | 119 | console.log('Wallet Response:', JSON.stringify(response.data)); 120 | 121 | if (response.data.return === true) { 122 | return res.json({ 123 | success: true, 124 | balance: response.data.wallet, 125 | currency: 'INR' 126 | }); 127 | } 128 | 129 | return res.status(500).json({ 130 | success: false, 131 | error: 'Failed to fetch wallet balance', 132 | response: response.data 133 | }); 134 | 135 | } catch (error) { 136 | console.error('WALLET CHECK ERROR:', error); 137 | 138 | let errorDetails = { message: error.message }; 139 | if (error.response) { 140 | errorDetails.status = error.response.status; 141 | errorDetails.data = error.response.data; 142 | } 143 | 144 | return res.status(500).json({ 145 | success: false, 146 | error: errorDetails 147 | }); 148 | } 149 | }); 150 | 151 | // Start server 152 | const PORT = 3002; // Use a different port to avoid conflicts 153 | app.listen(PORT, () => { 154 | console.log(` 155 | Fast2SMS Test Server 156 | ============================ 157 | Server running at http://localhost:${PORT} 158 | Health check: http://localhost:${PORT}/health 159 | Direct SMS test: http://localhost:${PORT}/direct-test 160 | Wallet balance: http://localhost:${PORT}/wallet-balance 161 | 162 | FAST2SMS_API_KEY is ${process.env.FAST2SMS_API_KEY ? 'configured' : 'NOT configured'} 163 | SMS_ENABLED is ${process.env.SMS_ENABLED === 'true' ? 'true' : 'not enabled'} 164 | `); 165 | }); 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fee Management System 2 | 3 | A comprehensive fee management system for educational institutes, featuring SMS notifications, admin panel, and student fee tracking. 4 | 5 | ## Overview 6 | 7 | The Fee Management System is designed to streamline fee collection and management for educational institutes. It provides automated fee reminders via SMS, a user-friendly admin panel, and comprehensive student record management. 8 | 9 | ## Features 10 | 11 | - **Automated Fee Reminders**: Schedule SMS notifications for upcoming fee payments 12 | - **Admin Dashboard**: Monitor payment status, student records, and reminders 13 | - **Student Management**: Track student details, courses, and fee schedules 14 | - **Payment Processing**: Record and manage fee payments 15 | - **Notification System**: Customizable SMS templates and delivery tracking 16 | - **Authentication**: Secure admin login system 17 | 18 | ## Project Structure 19 | 20 | ``` 21 | fee-management-system/ 22 | ├── backend/ # AWS Lambda functions and API endpoints 23 | │ ├── serverless.yml # AWS infrastructure configuration 24 | │ ├── src/ # Source code for backend services 25 | │ │ ├── app.js # Express app setup 26 | │ │ ├── handlers/ # API route handlers 27 | │ │ ├── lambda/ # Lambda functions 28 | │ │ ├── models/ # Data models 29 | │ │ └── utils/ # Utility functions 30 | │ ├── config/ # Environment configuration templates 31 | │ ├── docs/ # Documentation files 32 | │ ├── tests/ # Test scripts and utilities 33 | │ ├── utils/ # Utility scripts for maintenance 34 | │ └── package.json # Backend dependencies 35 | ├── database/ # Database schema and migrations 36 | │ └── schema.sql # SQL schema for reference 37 | └── frontend/ # Admin panel web application 38 | └── admin/ # React-based admin interface 39 | ``` 40 | 41 | ## Technology Stack 42 | 43 | ### Backend 44 | - **Node.js**: Server-side JavaScript runtime 45 | - **Express.js**: Web application framework 46 | - **AWS Lambda**: Serverless compute service 47 | - **DynamoDB**: NoSQL database service 48 | - **Serverless Framework**: Infrastructure as code 49 | - **Fast2SMS**: SMS delivery provider 50 | 51 | ### Frontend 52 | - **React.js**: User interface library 53 | - **Amazon S3**: Static website hosting 54 | - **CloudFront**: Content delivery network 55 | 56 | ## Deployment 57 | 58 | The system is deployed across multiple AWS services: 59 | 60 | 1. **Backend API**: AWS Lambda + API Gateway 61 | 62 | 2. **Admin Panel**: Amazon S3 + CloudFront 63 | - URL: admin.aiict.in 64 | 65 | 3. **Database**: Amazon DynamoDB 66 | - Tables: Students-pro, NotificationLogs-pro 67 | 68 | ## SMS Notification System 69 | 70 | The system sends three types of automated reminders: 71 | 72 | 1. **Due Tomorrow**: For students with fees due the next day 73 | 2. **Due in 3 Days**: Early reminders for upcoming payments 74 | 3. **Overdue**: Reminders for missed payments 75 | 76 | All SMS notifications are logged in the NotificationLogs-pro table for auditing and tracking. 77 | 78 | ## Environment Variables 79 | 80 | The system uses the following environment variables: 81 | 82 | ``` 83 | JWT_SECRET= 84 | ADMIN_EMAIL=YOUR_ADMIN_EMAIL 85 | ADMIN_PASSWORD_HASH= 86 | CORS_ORIGIN=YOUR_FRONTEND_URL 87 | FAST2SMS_API_KEY= 88 | SMS_ENABLED=true 89 | INSTITUTE_NAME=YOUR_INSTITUTE_NAME 90 | SUPPORT_PHONE= 91 | ``` 92 | 93 | ## Maintenance 94 | 95 | ### AWS Lambda Functions 96 | 97 | - **dailyFeeReminder**: Runs daily at 6:30 AM IST to send reminders 98 | - **api**: Handles all admin panel API requests 99 | 100 | ### Database Schema 101 | 102 | - **Students-pro**: Contains student records, courses, and fee details 103 | - **NotificationLogs-pro**: Tracks all SMS notifications sent 104 | 105 | ## Recent Optimizations 106 | 107 | 1. **SMS Cost Reduction**: Replaced Unicode symbols with ASCII to reduce SMS segments 108 | 2. **API Performance**: Implemented better error handling and connection pooling 109 | 3. **Notification System**: Eliminated duplicate messages and unnecessary admin notifications 110 | 4. **Fast2SMS Integration**: Fixed API method from POST to GET for proper delivery 111 | 112 | ## Troubleshooting 113 | 114 | ### Common Issues 115 | 116 | 1. **Missing SMS Notifications**: 117 | - Check if SMS_ENABLED is set to 'true' 118 | - Verify FAST2SMS_API_KEY is correct 119 | - Ensure phone numbers are in correct format (10 digits) 120 | 121 | 2. **Admin Panel Access Issues**: 122 | - Verify CORS settings in serverless.yml 123 | - Check JWT_SECRET for authentication 124 | 125 | 3. **Lambda Function Errors**: 126 | - Review CloudWatch logs for detailed error messages 127 | - Check environment variables in AWS Lambda configuration 128 | 129 | ## Future Enhancements 130 | 131 | 1. **Payment Gateway Integration**: Online fee payment capabilities 132 | 2. **Student Portal**: Self-service access for students 133 | 3. **Report Generation**: Advanced analytics and reporting features 134 | 4. **Multi-Institute Support**: Support for multiple branches or institutions 135 | 136 | ## License 137 | 138 | Proprietary - All rights reserved 139 | 140 | ## Contact 141 | 142 | For support or inquiries, contacts available at profile 143 | -------------------------------------------------------------------------------- /backend/src/handlers/export.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const express = require('express'); 3 | const ExcelJS = require('exceljs'); 4 | const router = express.Router(); 5 | 6 | // GET /api/export/students - Export students data as Excel file 7 | router.get('/students', async (req, res) => { 8 | try { 9 | // Configure AWS 10 | AWS.config.update({ 11 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 12 | }); 13 | 14 | const docClient = new AWS.DynamoDB.DocumentClient(); 15 | 16 | // Get all students 17 | const studentsTable = 'Students'; 18 | const studentsResult = await docClient.scan({ TableName: studentsTable }).promise(); 19 | const students = studentsResult.Items || []; 20 | 21 | // Create Excel workbook and worksheet 22 | const workbook = new ExcelJS.Workbook(); 23 | const worksheet = workbook.addWorksheet('Students'); 24 | 25 | // Define columns 26 | worksheet.columns = [ 27 | { header: 'ID', key: 'id', width: 20 }, 28 | { header: 'Name', key: 'name', width: 25 }, 29 | { header: 'Email', key: 'email', width: 30 }, 30 | { header: 'Phone', key: 'phone', width: 15 }, 31 | { header: 'Course', key: 'course', width: 20 }, 32 | { header: 'Batch Time', key: 'batchTime', width: 15 }, 33 | { header: 'Monthly Fee', key: 'monthlyFee', width: 15 }, 34 | { header: 'Fee Status', key: 'feeStatus', width: 15 }, 35 | { header: 'Due Date', key: 'dueDate', width: 15 }, 36 | { header: 'Course Duration (Months)', key: 'courseDuration', width: 20 }, 37 | { header: 'Registration Date', key: 'createdAt', width: 20 } 38 | ]; 39 | 40 | // Add rows 41 | students.forEach(student => { 42 | worksheet.addRow({ 43 | id: student.id || student.studentId, 44 | name: student.name, 45 | email: student.email, 46 | phone: student.phone, 47 | course: student.course, 48 | batchTime: student.batchTime, 49 | monthlyFee: student.fees && student.fees.monthly_amount, 50 | feeStatus: student.fees && student.fees.status, 51 | dueDate: student.fees && student.fees.due_date, 52 | courseDuration: student.courseDuration || 'N/A', 53 | createdAt: student.created_at 54 | }); 55 | }); 56 | 57 | // Style the header row 58 | worksheet.getRow(1).font = { bold: true }; 59 | worksheet.getRow(1).fill = { 60 | type: 'pattern', 61 | pattern: 'solid', 62 | fgColor: { argb: 'FFE0E0E0' } 63 | }; 64 | 65 | // Set response headers 66 | res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 67 | res.setHeader('Content-Disposition', 'attachment; filename="students.xlsx"'); 68 | 69 | // Write to buffer and send 70 | await workbook.xlsx.write(res); 71 | res.end(); 72 | 73 | } catch (error) { 74 | console.error('Error exporting students data:', error); 75 | res.status(500).json({ 76 | success: false, 77 | message: 'Error exporting students data', 78 | error: error.message 79 | }); 80 | } 81 | }); 82 | 83 | // GET /api/export/notifications - Export notification logs as Excel file 84 | router.get('/notifications', async (req, res) => { 85 | try { 86 | // Configure AWS 87 | AWS.config.update({ 88 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 89 | }); 90 | 91 | const docClient = new AWS.DynamoDB.DocumentClient(); 92 | 93 | // Get all notification logs 94 | const notificationLogsTable = 'NotificationLogs'; 95 | const logsResult = await docClient.scan({ TableName: notificationLogsTable }).promise(); 96 | const logs = logsResult.Items || []; 97 | 98 | // Create Excel workbook and worksheet 99 | const workbook = new ExcelJS.Workbook(); 100 | const worksheet = workbook.addWorksheet('Notification Logs'); 101 | 102 | // Define columns 103 | worksheet.columns = [ 104 | { header: 'ID', key: 'id', width: 20 }, 105 | { header: 'Student ID', key: 'studentId', width: 20 }, 106 | { header: 'Student Name', key: 'studentName', width: 25 }, 107 | { header: 'Message', key: 'message', width: 50 }, 108 | { header: 'Status', key: 'status', width: 15 }, 109 | { header: 'Created At', key: 'createdAt', width: 20 } 110 | ]; 111 | 112 | // Add rows 113 | logs.forEach(log => { 114 | worksheet.addRow({ 115 | id: log.id, 116 | studentId: log.student_id, 117 | studentName: log.student_name, 118 | message: log.message, 119 | status: log.status, 120 | createdAt: log.created_at 121 | }); 122 | }); 123 | 124 | // Style the header row 125 | worksheet.getRow(1).font = { bold: true }; 126 | worksheet.getRow(1).fill = { 127 | type: 'pattern', 128 | pattern: 'solid', 129 | fgColor: { argb: 'FFE0E0E0' } 130 | }; 131 | 132 | // Set response headers 133 | res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 134 | res.setHeader('Content-Disposition', 'attachment; filename="notification_logs.xlsx"'); 135 | 136 | // Write to buffer and send 137 | await workbook.xlsx.write(res); 138 | res.end(); 139 | 140 | } catch (error) { 141 | console.error('Error exporting notification logs:', error); 142 | res.status(500).json({ 143 | success: false, 144 | message: 'Error exporting notification logs', 145 | error: error.message 146 | }); 147 | } 148 | }); 149 | 150 | module.exports = router; 151 | -------------------------------------------------------------------------------- /backend/tests/test-reminder-system.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SMS Reminder System Test Script 3 | * 4 | * This script performs a comprehensive test of the SMS reminder system: 5 | * 1. Creates a test student with tomorrow's due date 6 | * 2. Verifies the student record was created 7 | * 3. Manually triggers the fee reminder function 8 | * 4. Reports the results 9 | */ 10 | 11 | // Load environment variables 12 | require('dotenv').config({ path: '.env.local' }); 13 | 14 | const AWS = require('aws-sdk'); 15 | const moment = require('moment'); 16 | const { v4: uuidv4 } = require('uuid'); 17 | 18 | // Configure AWS 19 | AWS.config.update({ 20 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 21 | }); 22 | 23 | const dynamoDB = new AWS.DynamoDB.DocumentClient(); 24 | const lambda = new AWS.Lambda(); 25 | 26 | // Configurations 27 | const STUDENT_TABLE = process.env.STUDENT_TABLE || 'Students-pro'; 28 | const FEE_TABLE = process.env.FEE_TABLE || 'Students-pro'; // We store fees in the same table 29 | const PHONE_NUMBER = process.argv[2] || 'YOUR_TEST_PHONE'; // Default or command line arg 30 | const STUDENT_NAME = 'TEST STUDENT'; 31 | 32 | async function runTest() { 33 | console.log(' Starting SMS Reminder System Test'); 34 | console.log('═════════════════════════════════════'); 35 | console.log(`Testing with phone number: +91${PHONE_NUMBER}`); 36 | 37 | // Step 1: Create test student 38 | console.log('\n Step 1: Creating test student record...'); 39 | const studentId = await createTestStudent(); 40 | 41 | if (!studentId) { 42 | console.error(' Failed to create test student. Exiting test.'); 43 | return; 44 | } 45 | 46 | // Step 2: Verify student record 47 | console.log('\n Step 2: Verifying student record...'); 48 | const student = await getStudentById(studentId); 49 | 50 | if (!student) { 51 | console.error(' Student record verification failed. Exiting test.'); 52 | return; 53 | } 54 | 55 | console.log(' Student record verified:', { 56 | id: student.id, 57 | name: student.name, 58 | phone: student.phone 59 | }); 60 | 61 | // Step 3: Manually trigger fee reminder 62 | console.log('\n Step 3: Triggering fee reminder function...'); 63 | const reminderResult = await triggerFeeReminder(); 64 | 65 | if (!reminderResult.success) { 66 | console.error(' Fee reminder function failed:', reminderResult.error); 67 | } else { 68 | console.log(' Fee reminder function executed with status:', reminderResult.statusCode); 69 | } 70 | 71 | // Step 4: Summary 72 | console.log('\n Test Summary'); 73 | console.log('═════════════════'); 74 | console.log('1. Test student created with ID:', studentId); 75 | console.log('2. Phone number to receive SMS:', `+91${PHONE_NUMBER}`); 76 | console.log('3. Due date set to:', moment().add(1, 'days').format('YYYY-MM-DD')); 77 | console.log('4. Fee reminder function triggered:', reminderResult.success ? ' Success' : ' Failed'); 78 | 79 | console.log('\n CHECK YOUR PHONE NOW'); 80 | console.log('If the system is working correctly, you should receive an SMS reminder within a few minutes.'); 81 | console.log('\n If you don\'t receive an SMS, try these troubleshooting steps:'); 82 | console.log('1. Check if the student record was created with the correct phone number'); 83 | console.log('2. Verify Fast2SMS API key is correctly configured'); 84 | console.log('3. Check Lambda function logs in AWS CloudWatch'); 85 | } 86 | 87 | async function createTestStudent() { 88 | const studentId = uuidv4(); 89 | const tomorrow = moment().add(1, 'days').format('YYYY-MM-DD'); 90 | 91 | // Create student record with fees included in the same record 92 | // Based on the actual schema used in the deployed system 93 | const studentParams = { 94 | TableName: STUDENT_TABLE, 95 | Item: { 96 | id: studentId, 97 | name: STUDENT_NAME, 98 | phone: PHONE_NUMBER, 99 | recordType: 'STUDENT', 100 | createdAt: new Date().toISOString(), 101 | updatedAt: new Date().toISOString(), 102 | fees: [ 103 | { 104 | id: uuidv4(), 105 | amount: 1000, // Amount in Rs 106 | dueDate: tomorrow, 107 | status: 'PENDING', 108 | description: 'TEST FEE ENTRY', 109 | createdAt: new Date().toISOString() 110 | } 111 | ], 112 | // Add other required fields based on the system's needs 113 | status: 'ACTIVE', 114 | paymentHistory: [], 115 | notifications: [] 116 | } 117 | }; 118 | 119 | try { 120 | await dynamoDB.put(studentParams).promise(); 121 | console.log(' Student record created successfully with fee due date:', tomorrow); 122 | 123 | return studentId; 124 | } catch (error) { 125 | console.error(' Error creating test records:', error); 126 | return null; 127 | } 128 | } 129 | 130 | async function getStudentById(id) { 131 | const params = { 132 | TableName: STUDENT_TABLE, 133 | Key: { id } 134 | }; 135 | 136 | try { 137 | const result = await dynamoDB.get(params).promise(); 138 | return result.Item; 139 | } catch (error) { 140 | console.error(' Error fetching student:', error); 141 | return null; 142 | } 143 | } 144 | 145 | async function triggerFeeReminder() { 146 | const params = { 147 | FunctionName: 'YOUR_LAMBDA_FUNCTION_NAME', 148 | InvocationType: 'RequestResponse', 149 | Payload: JSON.stringify({ 150 | source: 'manual-test', 151 | timestamp: new Date().toISOString() 152 | }) 153 | }; 154 | 155 | try { 156 | const result = await lambda.invoke(params).promise(); 157 | return { 158 | success: true, 159 | statusCode: result.StatusCode, 160 | response: result.Payload ? JSON.parse(result.Payload) : null 161 | }; 162 | } catch (error) { 163 | return { 164 | success: false, 165 | error: error.message 166 | }; 167 | } 168 | } 169 | 170 | // Run the test 171 | runTest(); 172 | -------------------------------------------------------------------------------- /backend/src/handlers/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const crypto = require('crypto'); 4 | const jwt = require('jsonwebtoken'); 5 | 6 | // Secure credentials with environment variables support 7 | // In production, add these to your environment variables 8 | const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'YOUR_ADMIN_EMAIL'; 9 | // Password: Admin@123 (securely hashed with salt) 10 | const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || 'YOUR_PASSWORD_HASH'; // Legacy hash without salt 11 | const SALT = process.env.PASSWORD_SALT || 'YOUR_SALT'; // Add a salt for better security 12 | const JWT_SECRET = process.env.JWT_SECRET || 'YOUR_JWT_SECRET'; 13 | 14 | // Helper function to hash passwords with salt 15 | const hashPassword = (password) => { 16 | // For backward compatibility, we support both salted and unsalted hashes 17 | // New passwords should use the salted version 18 | return crypto.createHash('sha256').update(password + SALT).digest('hex'); 19 | }; 20 | 21 | // Helper to check if a password is valid against legacy or new hashing 22 | const validatePassword = (password, storedHash) => { 23 | // Try salted hash first (new method) 24 | const saltedHash = crypto.createHash('sha256').update(password + SALT).digest('hex'); 25 | if (saltedHash === storedHash) return true; 26 | 27 | // If salted hash doesn't match, try legacy hash (without salt) 28 | const legacyHash = crypto.createHash('sha256').update(password).digest('hex'); 29 | return legacyHash === storedHash; 30 | }; 31 | 32 | // Simple rate limiting 33 | const loginAttempts = {}; 34 | const MAX_ATTEMPTS = 5; 35 | const LOCKOUT_TIME = 15 * 60 * 1000; // 15 minutes in milliseconds 36 | 37 | // Login endpoint 38 | router.post('/login', (req, res) => { 39 | try { 40 | const { email, password } = req.body; 41 | const ipAddress = req.ip || 'unknown'; 42 | 43 | // Basic validation 44 | if (!email || !password) { 45 | return res.status(400).json({ 46 | success: false, 47 | message: 'Email and password are required' 48 | }); 49 | } 50 | 51 | // Check for rate limiting 52 | const attemptKey = `${ipAddress}:${email.toLowerCase()}`; 53 | const now = Date.now(); 54 | 55 | // Initialize or get existing attempts 56 | if (!loginAttempts[attemptKey]) { 57 | loginAttempts[attemptKey] = { count: 0, lastAttempt: now, lockedUntil: 0 }; 58 | } 59 | 60 | const attempt = loginAttempts[attemptKey]; 61 | 62 | // Check if account is locked 63 | if (attempt.lockedUntil > now) { 64 | const remainingTime = Math.ceil((attempt.lockedUntil - now) / 1000 / 60); 65 | return res.status(429).json({ 66 | success: false, 67 | message: `Too many failed attempts. Please try again in ${remainingTime} minutes.` 68 | }); 69 | } 70 | 71 | // Check credentials using our validation function that supports both hash formats 72 | const isValidPassword = validatePassword(password, ADMIN_PASSWORD_HASH); 73 | 74 | if (email === ADMIN_EMAIL && isValidPassword) { 75 | // Reset login attempts on success 76 | loginAttempts[attemptKey] = { count: 0, lastAttempt: now, lockedUntil: 0 }; 77 | 78 | // Create JWT token with shorter expiration for security 79 | const token = jwt.sign( 80 | { email, role: 'admin' }, 81 | JWT_SECRET, 82 | { expiresIn: '8h' } 83 | ); 84 | 85 | // Send success response 86 | return res.json({ 87 | success: true, 88 | message: 'Login successful', 89 | token, 90 | user: { 91 | email, 92 | name: 'Admin', 93 | role: 'admin' 94 | } 95 | }); 96 | } else { 97 | // Increment failed attempt counter 98 | attempt.count += 1; 99 | attempt.lastAttempt = now; 100 | 101 | // Check if we should lock the account 102 | if (attempt.count >= MAX_ATTEMPTS) { 103 | attempt.lockedUntil = now + LOCKOUT_TIME; 104 | return res.status(429).json({ 105 | success: false, 106 | message: `Too many failed attempts. Account locked for ${LOCKOUT_TIME/60000} minutes.` 107 | }); 108 | } 109 | 110 | // Invalid credentials 111 | return res.status(401).json({ 112 | success: false, 113 | message: `Invalid email or password. ${MAX_ATTEMPTS - attempt.count} attempts remaining.` 114 | }); 115 | } 116 | } catch (error) { 117 | console.error('Error during login:', error); 118 | return res.status(500).json({ 119 | success: false, 120 | message: 'Internal server error', 121 | error: error.message 122 | }); 123 | } 124 | }); 125 | 126 | // Verify token endpoint (useful for frontend to check if token is valid) 127 | router.post('/verify', (req, res) => { 128 | try { 129 | const { token } = req.body; 130 | 131 | if (!token) { 132 | return res.status(400).json({ 133 | success: false, 134 | message: 'Token is required' 135 | }); 136 | } 137 | 138 | // Verify token 139 | jwt.verify(token, JWT_SECRET, (err, decoded) => { 140 | if (err) { 141 | return res.status(401).json({ 142 | success: false, 143 | message: 'Invalid or expired token' 144 | }); 145 | } 146 | 147 | return res.json({ 148 | success: true, 149 | message: 'Token is valid', 150 | user: { 151 | email: decoded.email, 152 | name: 'Admin', 153 | role: decoded.role 154 | } 155 | }); 156 | }); 157 | } catch (error) { 158 | console.error('Error verifying token:', error); 159 | return res.status(500).json({ 160 | success: false, 161 | message: 'Internal server error', 162 | error: error.message 163 | }); 164 | } 165 | }); 166 | 167 | module.exports = router; 168 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/pages/Login.js: -------------------------------------------------------------------------------- 1 | // Create file at: src/pages/Login.js 2 | import React, { useState, useEffect } from 'react'; 3 | import { Form, Button, Card, Container, Alert, Row, Col } from 'react-bootstrap'; 4 | import { useNavigate } from 'react-router-dom'; 5 | import { API_BASE_URL } from '../config'; 6 | 7 | const Login = () => { 8 | const navigate = useNavigate(); 9 | const [credentials, setCredentials] = useState({ email: '', password: '' }); 10 | const [loading, setLoading] = useState(false); 11 | const [error, setError] = useState(''); 12 | 13 | // Check if user is already logged in on component mount 14 | useEffect(() => { 15 | const token = localStorage.getItem('authToken'); 16 | 17 | if (token) { 18 | // Verify token validity 19 | fetch(`${API_BASE_URL}/api/auth/verify`, { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json' 23 | }, 24 | body: JSON.stringify({ token }) 25 | }) 26 | .then(res => res.json()) 27 | .then(data => { 28 | if (data.success) { 29 | // Token is valid, redirect to dashboard 30 | navigate('/'); 31 | } else { 32 | // Token is invalid, clear it 33 | localStorage.removeItem('authToken'); 34 | localStorage.removeItem('user'); 35 | } 36 | }) 37 | .catch(err => { 38 | console.error('Token verification failed:', err); 39 | localStorage.removeItem('authToken'); 40 | localStorage.removeItem('user'); 41 | }); 42 | } 43 | }, [navigate]); 44 | 45 | const handleInputChange = (e) => { 46 | setCredentials({ 47 | ...credentials, 48 | [e.target.name]: e.target.value 49 | }); 50 | }; 51 | 52 | // Track login attempts 53 | const [failedAttempts, setFailedAttempts] = useState(0); 54 | 55 | const handleSubmit = async (e) => { 56 | e.preventDefault(); 57 | setLoading(true); 58 | setError(''); 59 | 60 | // Add a delay if there were previous failed attempts (prevents brute forcing) 61 | if (failedAttempts > 0) { 62 | // Exponential backoff - wait longer for each failed attempt 63 | const delay = Math.min(2000 * Math.pow(1.5, failedAttempts - 1), 10000); 64 | await new Promise(resolve => setTimeout(resolve, delay)); 65 | } 66 | 67 | // Use the real authentication API 68 | fetch(`${API_BASE_URL}/api/auth/login`, { 69 | method: 'POST', 70 | headers: { 'Content-Type': 'application/json' }, 71 | body: JSON.stringify(credentials), 72 | cache: 'no-cache' // Prevent caching of login responses 73 | }) 74 | .then(res => res.json()) 75 | .then(data => { 76 | if (data.success) { 77 | // Reset failed attempts counter 78 | setFailedAttempts(0); 79 | 80 | // Store token and user info 81 | localStorage.setItem('authToken', data.token); 82 | localStorage.setItem('user', JSON.stringify(data.user)); 83 | 84 | // Store token expiry time (based on 8h expiry) 85 | const expiryTime = new Date(); 86 | expiryTime.setHours(expiryTime.getHours() + 8); 87 | localStorage.setItem('authTokenExpiry', expiryTime.toISOString()); 88 | 89 | // Redirect to dashboard 90 | navigate('/'); 91 | } else { 92 | // Increment failed attempts counter 93 | setFailedAttempts(prevAttempts => prevAttempts + 1); 94 | setError(data.message || 'Login failed. Check your credentials.'); 95 | } 96 | }) 97 | .catch(err => { 98 | console.error('Login error:', err); 99 | setError('Login failed. Please check your internet connection and try again.'); 100 | // Still increment failed attempts on network error 101 | setFailedAttempts(prevAttempts => prevAttempts + 1); 102 | }) 103 | .finally(() => setLoading(false)); 104 | }; 105 | 106 | return ( 107 | 108 | 109 | 110 | 111 | Login 112 | 113 | {error && {error}} 114 | 115 |
116 | 117 | Email 118 | 126 | 127 | 128 | 129 | Password 130 | 138 | 139 | 140 |
141 | 148 |
149 |
150 |
151 |
152 | 153 |
154 |
155 | ); 156 | }; 157 | 158 | export default Login; -------------------------------------------------------------------------------- /backend/src/handlers/fixed-export.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const ExcelJS = require('exceljs'); 3 | 4 | // Configure AWS 5 | AWS.config.update({ 6 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 7 | }); 8 | 9 | // Create DynamoDB DocumentClient 10 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 11 | 12 | exports.exportStudents = async (req, res) => { 13 | try { 14 | // Create a new workbook 15 | const workbook = new ExcelJS.Workbook(); 16 | const worksheet = workbook.addWorksheet('Students'); 17 | 18 | // Define columns 19 | worksheet.columns = [ 20 | { header: 'ID', key: 'id', width: 36 }, 21 | { header: 'Registration No', key: 'registration_no', width: 20 }, 22 | { header: 'Name', key: 'name', width: 30 }, 23 | { header: "Father's Name", key: 'fathers_name', width: 30 }, 24 | { header: 'Phone', key: 'phone_no', width: 15 }, 25 | { header: 'Email', key: 'email', width: 25 }, 26 | { header: 'Course', key: 'course_name', width: 15 }, 27 | { header: 'Batch Time', key: 'batch_time', width: 20 }, 28 | { header: 'Joining Date', key: 'created_at', width: 15 }, 29 | { header: 'Monthly Fee', key: 'monthly_fee', width: 15 }, 30 | { header: 'Fee Status', key: 'fee_status', width: 15 }, 31 | { header: 'Due Date', key: 'due_date', width: 15 }, 32 | { header: 'Course Duration (Months)', key: 'course_duration', width: 20 } 33 | ]; 34 | 35 | // Add header row styling 36 | worksheet.getRow(1).font = { bold: true }; 37 | worksheet.getRow(1).fill = { 38 | type: 'pattern', 39 | pattern: 'solid', 40 | fgColor: { argb: 'FFD3D3D3' } 41 | }; 42 | 43 | // Query DynamoDB for students 44 | const params = { 45 | TableName: process.env.STUDENTS_TABLE || 'Students' 46 | }; 47 | 48 | const result = await dynamodb.scan(params).promise(); 49 | console.log(`Retrieved ${result.Items.length} students from database`); 50 | 51 | // Add rows to worksheet 52 | for (const student of result.Items) { 53 | worksheet.addRow({ 54 | id: student.id, 55 | registration_no: student.registration_no || '', 56 | name: student.name || '', 57 | fathers_name: student.fathers_name || '', 58 | phone_no: student.phone_no || student.phone || '', 59 | email: student.email || '', 60 | course_name: student.course_name || student.course || '', 61 | batch_time: student.batch_time || student.batchTime || '', 62 | created_at: student.created_at || '', 63 | monthly_fee: student.fees?.monthly_amount || '', 64 | fee_status: student.fees?.status || '', 65 | due_date: student.fees?.due_date || '', 66 | course_duration: student.course_duration || '' 67 | }); 68 | } 69 | // Set response headers for file download 70 | res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 71 | res.setHeader('Content-Disposition', 'attachment; filename=students.xlsx'); 72 | res.setHeader('Access-Control-Allow-Origin', '*'); 73 | 74 | // Generate buffer and send 75 | const buffer = await workbook.xlsx.writeBuffer(); 76 | res.send(buffer); 77 | 78 | console.log("Excel file sent successfully"); 79 | } catch (error) { 80 | console.error('Error exporting students:', error); 81 | res.status(500).json({ 82 | success: false, 83 | message: 'Failed to export students', 84 | error: error.message 85 | }); 86 | } 87 | }; 88 | 89 | exports.exportNotifications = async (req, res) => { 90 | try { 91 | // Create a new workbook 92 | const workbook = new ExcelJS.Workbook(); 93 | const worksheet = workbook.addWorksheet('Notifications'); 94 | 95 | // Define columns 96 | worksheet.columns = [ 97 | { header: 'ID', key: 'id', width: 36 }, 98 | { header: 'Student ID', key: 'student_id', width: 36 }, 99 | { header: 'Student Name', key: 'student_name', width: 30 }, 100 | { header: 'Phone Number', key: 'phone_no', width: 20 }, 101 | { header: 'Status', key: 'status', width: 15 }, 102 | { header: 'Message', key: 'message', width: 40 }, 103 | { header: 'Sent At', key: 'created_at', width: 20 } 104 | ]; 105 | 106 | // Add header row styling 107 | worksheet.getRow(1).font = { bold: true }; 108 | worksheet.getRow(1).fill = { 109 | type: 'pattern', 110 | pattern: 'solid', 111 | fgColor: { argb: 'FFD3D3D3' } 112 | }; 113 | 114 | // Query DynamoDB for notification logs 115 | const params = { 116 | TableName: process.env.NOTIFICATION_LOGS_TABLE || 'NotificationLogs' 117 | }; 118 | 119 | const result = await dynamodb.scan(params).promise(); 120 | console.log(`Retrieved ${result.Items.length} notification logs from database`); 121 | 122 | // Add rows to worksheet 123 | for (const log of result.Items) { 124 | worksheet.addRow({ 125 | id: log.id, 126 | student_id: log.student_id || '', 127 | student_name: log.student_name || '', 128 | phone_no: log.phone_no || '', 129 | status: log.status || '', 130 | message: log.message || '', 131 | created_at: log.created_at || '' 132 | }); 133 | } 134 | // Set response headers for file download 135 | res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 136 | res.setHeader('Content-Disposition', 'attachment; filename=notification_logs.xlsx'); 137 | res.setHeader('Access-Control-Allow-Origin', '*'); 138 | 139 | // Generate buffer and send 140 | const buffer = await workbook.xlsx.writeBuffer(); 141 | res.send(buffer); 142 | 143 | console.log("Notifications Excel file sent successfully"); 144 | } catch (error) { 145 | console.error('Error exporting notification logs:', error); 146 | res.status(500).json({ 147 | success: false, 148 | message: 'Failed to export notification logs', 149 | error: error.message 150 | }); 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /backend/src/handlers/student.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Student = require('../models/Student'); 4 | const { notifyWelcomeStudent } = require('../utils/notification'); 5 | 6 | // POST /api/students - Create new student 7 | router.post('/', async (req, res) => { 8 | const { 9 | name, 10 | fathersName, 11 | registrationNo, 12 | phoneNo, 13 | courseName, 14 | batchTime, 15 | monthlyAmount, 16 | dueDate, 17 | joining_date, // Extract joining_date from the request 18 | courseDuration 19 | } = req.body; 20 | 21 | // Log incoming request for debugging 22 | console.log('Creating student with data:', { 23 | name, courseName, batchTime, dueDate, joining_date 24 | }); 25 | 26 | try { 27 | const newStudent = await Student.create({ 28 | name, 29 | fathersName, 30 | registrationNo, 31 | phoneNo, 32 | courseName, 33 | batchTime, 34 | monthlyAmount, 35 | dueDate, 36 | joining_date, // Pass joining_date to Student model 37 | courseDuration 38 | }); 39 | 40 | // Send welcome SMS to new student 41 | try { 42 | await notifyWelcomeStudent( 43 | phoneNo, 44 | name, 45 | courseName, 46 | batchTime 47 | ); 48 | } catch (notificationError) { 49 | console.error('Failed to send welcome message:', notificationError); 50 | // Don't fail the student creation if notification fails 51 | } 52 | 53 | res.status(201).json({ 54 | success: true, 55 | message: 'Student added successfully', 56 | student: newStudent 57 | }); 58 | } catch (error) { 59 | res.status(400).json({ 60 | success: false, 61 | message: error.message 62 | }); 63 | } 64 | }); 65 | 66 | // GET /api/students - Get all students 67 | router.get('/', async (req, res) => { 68 | try { 69 | const students = await Student.findAll(); 70 | res.status(200).json({ 71 | success: true, 72 | students: students 73 | }); 74 | } catch (error) { 75 | res.status(500).json({ 76 | success: false, 77 | message: error.message 78 | }); 79 | } 80 | }); 81 | 82 | // GET /api/students/:id - Get student by ID 83 | router.get('/:id', async (req, res) => { 84 | const { id } = req.params; 85 | 86 | try { 87 | const student = await Student.findById(id); 88 | if (student) { 89 | res.status(200).json({ 90 | success: true, 91 | student: student 92 | }); 93 | } else { 94 | res.status(404).json({ 95 | success: false, 96 | message: 'Student not found' 97 | }); 98 | } 99 | } catch (error) { 100 | res.status(500).json({ 101 | success: false, 102 | message: error.message 103 | }); 104 | } 105 | }); 106 | 107 | // PUT /api/students/:id - Update student 108 | router.put('/:id', async (req, res) => { 109 | const { id } = req.params; 110 | const { 111 | name, 112 | fathersName, 113 | registrationNo, 114 | phoneNo, 115 | courseName, 116 | batchTime, 117 | monthlyAmount, 118 | dueDate, 119 | feeStatus, 120 | courseDuration 121 | } = req.body; 122 | 123 | try { 124 | // First get the existing student to preserve values not being updated 125 | const existingStudent = await Student.findById(id); 126 | if (!existingStudent) { 127 | return res.status(404).json({ 128 | success: false, 129 | message: 'Student not found' 130 | }); 131 | } 132 | 133 | // Build update object with all possible fields 134 | const updateData = { 135 | name: name || existingStudent.name, 136 | fathersName: fathersName || existingStudent.fathers_name, 137 | registrationNo: registrationNo || existingStudent.registration_no, 138 | phoneNo: phoneNo || existingStudent.phone_no, 139 | courseName: courseName || existingStudent.course_name, 140 | courseDuration: courseDuration !== undefined ? courseDuration : (existingStudent.course_duration || 6), 141 | batchTime: batchTime || existingStudent.batch_time 142 | }; 143 | 144 | // Update the basic student information 145 | const updatedStudent = await Student.update(id, updateData); 146 | 147 | // Handle fee updates if provided 148 | if (monthlyAmount) { 149 | await Student.updateFeeAmount(id, monthlyAmount); 150 | } 151 | 152 | if (feeStatus) { 153 | await Student.updateFeeStatus(id, feeStatus); 154 | } 155 | 156 | if (dueDate) { 157 | await Student.updateFeeDueDate(id, dueDate); 158 | } 159 | 160 | // Get the fully updated student 161 | const finalStudent = await Student.findById(id); 162 | 163 | res.status(200).json({ 164 | success: true, 165 | message: 'Student updated successfully', 166 | student: finalStudent 167 | }); 168 | } catch (error) { 169 | res.status(400).json({ 170 | success: false, 171 | message: error.message 172 | }); 173 | } 174 | }); 175 | 176 | // DELETE /api/students/:id - Delete student 177 | router.delete('/:id', async (req, res) => { 178 | const { id } = req.params; 179 | 180 | try { 181 | await Student.delete(id); 182 | res.status(200).json({ 183 | success: true, 184 | message: 'Student deleted successfully' 185 | }); 186 | } catch (error) { 187 | res.status(500).json({ 188 | success: false, 189 | message: error.message 190 | }); 191 | } 192 | }); 193 | 194 | module.exports = router; -------------------------------------------------------------------------------- /backend/src/handlers/stats.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const express = require('express'); 3 | const router = express.Router(); 4 | 5 | // GET /api/stats - Get dashboard statistics 6 | router.get('/', async (req, res) => { 7 | try { 8 | // Do basic AWS configuration with region from environment 9 | AWS.config.update({ 10 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 11 | }); 12 | 13 | // Create separate DynamoDB client to isolate any issues 14 | const dynamoDb = new AWS.DynamoDB(); 15 | const docClient = new AWS.DynamoDB.DocumentClient(); 16 | 17 | // First, list all tables for debug purposes 18 | console.log('Attempting to list all DynamoDB tables...'); 19 | try { 20 | const tables = await dynamoDb.listTables({}).promise(); 21 | console.log('Available tables:', tables.TableNames); 22 | } catch (error) { 23 | console.error('Error listing tables:', error); 24 | // Continue with hardcoded table names even if listing fails 25 | } // Get all students from DynamoDB 26 | // Use the actual table names from DynamoDB 27 | const studentsTable = 'Students'; 28 | console.log('Fetching students from table:', studentsTable); 29 | 30 | try { 31 | const studentsParams = { 32 | TableName: studentsTable 33 | }; 34 | 35 | const studentsResult = await docClient.scan(studentsParams).promise(); 36 | const students = studentsResult.Items || []; 37 | 38 | // Calculate statistics 39 | const totalStudents = students.length; 40 | 41 | // Count students with pending/overdue fees 42 | const pendingFees = students.filter(student => 43 | student.fees && (student.fees.status === 'pending' || student.fees.status === 'overdue') 44 | ).length; 45 | 46 | // Calculate total revenue (paid fees) 47 | let totalRevenue = 0; 48 | students.forEach(student => { 49 | if (student.fees && student.fees.monthly_amount && student.fees.status === 'paid') { 50 | totalRevenue += parseFloat(student.fees.monthly_amount); 51 | } 52 | }); 53 | 54 | // Get notifications from the last 24 hours 55 | const oneDayAgo = new Date(); 56 | oneDayAgo.setDate(oneDayAgo.getDate() - 1); 57 | const oneDayAgoString = oneDayAgo.toISOString(); 58 | 59 | let recentReminders = 0; 60 | try { 61 | // Use the actual table name from DynamoDB 62 | const notificationLogsTable = 'NotificationLogs'; 63 | console.log('Scanning notification logs from table:', notificationLogsTable); 64 | const logsParams = { 65 | TableName: notificationLogsTable, 66 | FilterExpression: "created_at >= :oneDayAgo", 67 | ExpressionAttributeValues: { 68 | ":oneDayAgo": oneDayAgoString 69 | } 70 | }; 71 | 72 | const logsResult = await docClient.scan(logsParams).promise(); 73 | recentReminders = logsResult.Items ? logsResult.Items.length : 0; 74 | } catch (notifError) { 75 | console.error('Error fetching notifications:', notifError); 76 | // Continue with zero if there's an error 77 | } 78 | 79 | // Calculate students with fees due tomorrow 80 | const tomorrow = new Date(); 81 | tomorrow.setDate(tomorrow.getDate() + 1); 82 | const tomorrowString = tomorrow.toISOString().split('T')[0]; 83 | 84 | const dueStudentsTomorrow = students.filter(student => { 85 | if (!student.fees || !student.fees.due_date) return false; 86 | const dueDate = student.fees.due_date.split('T')[0]; 87 | return dueDate === tomorrowString && student.fees.status !== 'paid'; 88 | }); 89 | // Calculate period-based revenue 90 | // Monthly revenue = current month's paid fees 91 | const currentDate = new Date(); 92 | const currentMonth = currentDate.getMonth(); 93 | const currentYear = currentDate.getFullYear(); 94 | 95 | // Calculate quarterly periods (quarters of the year) 96 | const currentQuarter = Math.floor(currentMonth / 3); 97 | const quarterStartMonth = currentQuarter * 3; 98 | const quarterStartDate = new Date(currentYear, quarterStartMonth, 1); 99 | 100 | // Filter students for monthly and quarterly revenue 101 | let monthlyRevenue = 0; 102 | let quarterlyRevenue = 0; 103 | let yearlyRevenue = 0; 104 | 105 | students.forEach(student => { 106 | if (student.fees && student.fees.monthly_amount && student.fees.status === 'paid' && student.fees.last_paid) { 107 | const paidDate = new Date(student.fees.last_paid); 108 | const paidMonth = paidDate.getMonth(); 109 | const paidYear = paidDate.getFullYear(); 110 | const amount = parseFloat(student.fees.monthly_amount); 111 | 112 | // Monthly revenue - current month 113 | if (paidMonth === currentMonth && paidYear === currentYear) { 114 | monthlyRevenue += amount; 115 | } 116 | 117 | // Quarterly revenue - current quarter 118 | if (paidDate >= quarterStartDate && paidYear === currentYear) { 119 | quarterlyRevenue += amount; 120 | } 121 | 122 | // Yearly revenue - current year 123 | if (paidYear === currentYear) { 124 | yearlyRevenue += amount; 125 | } 126 | } 127 | }); 128 | 129 | return res.status(200).json({ 130 | success: true, 131 | totalStudents, 132 | pendingFees, 133 | totalRevenue, 134 | monthlyRevenue, 135 | quarterlyRevenue, 136 | yearlyRevenue, 137 | recentReminders, 138 | dueStudentsTomorrow 139 | }); 140 | } catch (scanError) { 141 | console.error('Error scanning students table:', scanError); 142 | // Return fallback data instead of error 143 | return res.status(200).json({ 144 | success: true, 145 | totalStudents: 0, 146 | pendingFees: 0, 147 | totalRevenue: 0, 148 | recentReminders: 0, 149 | dueStudentsTomorrow: [] 150 | }); 151 | } 152 | } catch (error) { 153 | console.error('Error getting dashboard stats:', error); 154 | 155 | return res.status(500).json({ 156 | success: false, 157 | message: 'Error getting dashboard statistics', 158 | error: error.message 159 | }); 160 | } 161 | }); 162 | 163 | module.exports = router; 164 | -------------------------------------------------------------------------------- /backend/src/handlers/fees.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const Student = require('../models/Student'); 4 | const { notifyOwner, notifyStudent, notifyPaymentConfirmation } = require('../utils/notification'); 5 | 6 | // GET /api/fees/due - SELECT students with due fees 7 | router.get('/due', async (req, res) => { 8 | try { 9 | const dueStudents = await Student.getStudentsWithDueFees(); 10 | res.status(200).json({ 11 | success: true, 12 | students: dueStudents, 13 | count: dueStudents.length 14 | }); 15 | } catch (error) { 16 | res.status(500).json({ 17 | success: false, 18 | message: error.message 19 | }); 20 | } 21 | }); 22 | 23 | // PUT /api/fees/:id/status - UPDATE fee status 24 | router.put('/:id/status', async (req, res) => { 25 | const { id } = req.params; 26 | const { status } = req.body; 27 | 28 | try { 29 | const paidDate = status === 'paid' ? new Date().toISOString() : null; 30 | const updatedStudent = await Student.updateFeeStatus(id, status, paidDate); 31 | 32 | // Send confirmation SMS if fee marked as paid 33 | if (status === 'paid' && updatedStudent) { 34 | try { 35 | await notifyPaymentConfirmation( 36 | updatedStudent.phone_no, 37 | updatedStudent.name, 38 | updatedStudent.fees.monthly_amount, 39 | new Date().toLocaleDateString('en-IN') 40 | ); 41 | } catch (notificationError) { 42 | console.error('Failed to send payment confirmation:', notificationError); 43 | } 44 | } 45 | 46 | res.status(200).json({ 47 | success: true, 48 | message: 'Fee status updated successfully', 49 | student: updatedStudent 50 | }); 51 | } catch (error) { 52 | res.status(400).json({ 53 | success: false, 54 | message: error.message 55 | }); 56 | } 57 | }); 58 | 59 | // POST /api/fees/send-reminder/:id - Send manual fee reminder 60 | router.post('/send-reminder/:id', async (req, res) => { 61 | const { id } = req.params; 62 | 63 | try { 64 | const student = await Student.findById(id); 65 | if (!student) { 66 | return res.status(404).json({ 67 | success: false, 68 | message: 'Student not found' 69 | }); 70 | } 71 | 72 | const dueDate = new Date(student.fees.due_date).toLocaleDateString('en-IN'); 73 | const result = await notifyStudent( 74 | student.phone_no, 75 | student.name, 76 | student.fees.monthly_amount, 77 | dueDate 78 | ); 79 | 80 | res.status(200).json({ 81 | success: true, 82 | message: 'Reminder sent successfully', 83 | notificationResult: result 84 | }); 85 | } catch (error) { 86 | res.status(500).json({ 87 | success: false, 88 | message: error.message 89 | }); 90 | } 91 | }); 92 | 93 | // Function to check for students with fees due tomorrow (Enhanced) 94 | const checkDueFees = async (event, context) => { 95 | console.log('Starting daily fee check...'); 96 | 97 | try { 98 | const students = await Student.getStudentsWithDueFees(); 99 | console.log(`Found ${students.length} students with pending fees`); 100 | 101 | const today = new Date(); 102 | const tomorrow = new Date(today); 103 | tomorrow.setDate(today.getDate() + 1); 104 | 105 | // Filter students with fees due tomorrow 106 | const dueStudents = students.filter(student => { 107 | const dueDate = new Date(student.fees.due_date); 108 | return dueDate.toDateString() === tomorrow.toDateString() && 109 | student.fees.status !== 'paid'; 110 | }); 111 | 112 | console.log(`Found ${dueStudents.length} students with fees due tomorrow`); 113 | 114 | let successCount = 0; 115 | let failCount = 0; 116 | 117 | // Send notifications to students 118 | for (const student of dueStudents) { 119 | try { 120 | const dueDate = new Date(student.fees.due_date).toLocaleDateString('en-IN'); 121 | 122 | // Notify student 123 | await notifyStudent( 124 | student.phone_no, 125 | student.name, 126 | student.fees.monthly_amount, 127 | dueDate 128 | ); 129 | 130 | successCount++; 131 | console.log(`Notification sent to ${student.name} (${student.phone_no})`); 132 | } catch (error) { 133 | failCount++; 134 | console.error(`Failed to notify ${student.name}:`, error); 135 | } 136 | } 137 | 138 | // Log summary to CloudWatch instead of sending SMS to save costs 139 | if (dueStudents.length > 0) { 140 | const totalAmount = dueStudents.reduce((sum, s) => sum + (s.fees.monthly_amount || 0), 0); 141 | console.log(`Summary: ${dueStudents.length} students have fees due around ${tomorrow.toLocaleDateString('en-IN')}`); 142 | console.log(`Total pending amount: Rs.${totalAmount}`); 143 | 144 | // No SMS notification to owner - we'll rely on database logs instead 145 | // This saves costs and prevents unnecessary "X students" messages 146 | } 147 | 148 | const result = { 149 | totalDueStudents: dueStudents.length, 150 | successfulNotifications: successCount, 151 | failedNotifications: failCount, 152 | timestamp: new Date().toISOString(), 153 | students: dueStudents.map(s => ({ 154 | id: s.id, 155 | name: s.name, 156 | phone: s.phone_no, 157 | amount: s.fees.monthly_amount, 158 | dueDate: s.fees.due_date 159 | })) 160 | }; 161 | 162 | console.log('Daily fee check completed:', result); 163 | return result; 164 | 165 | } catch (error) { 166 | console.error('Error in daily fee check:', error); 167 | throw error; 168 | } 169 | }; 170 | 171 | module.exports = router; 172 | module.exports.checkDueFees = checkDueFees; -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/services/api.js: -------------------------------------------------------------------------------- 1 | import { API_BASE_URL } from '../config'; 2 | 3 | // Helper function for handling API responses 4 | const handleResponse = async (response) => { 5 | if (!response.ok) { 6 | const errorData = await response.json().catch(() => ({})); 7 | throw new Error(errorData.message || `API error: ${response.status}`); 8 | } 9 | return await response.json(); 10 | }; 11 | 12 | // Auth header helper 13 | const authHeader = () => { 14 | const token = localStorage.getItem('authToken'); 15 | return token ? { 'Authorization': `Bearer ${token}` } : {}; 16 | }; 17 | 18 | // Student API services 19 | export const StudentService = { 20 | // Get all students 21 | getAll: async () => { 22 | const response = await fetch(`${API_BASE_URL}/api/students`, { 23 | headers: { 24 | ...authHeader() 25 | } 26 | }); 27 | return handleResponse(response); 28 | }, 29 | 30 | // Get student by ID 31 | getById: async (id) => { 32 | const response = await fetch(`${API_BASE_URL}/api/students/${id}`, { 33 | headers: { 34 | ...authHeader() 35 | } 36 | }); 37 | return handleResponse(response); 38 | }, 39 | 40 | // Create a new student 41 | create: async (studentData) => { 42 | // Transform frontend form data to match backend expectations 43 | const payload = { 44 | name: studentData.name, 45 | fathersName: studentData.fathers_name, 46 | registrationNo: studentData.registration_no || Date.now().toString(), // Generate if not provided 47 | phoneNo: studentData.phone_no, 48 | courseName: studentData.course_name, 49 | batchTime: studentData.batch_time, 50 | monthlyAmount: Number(studentData.monthly_fee), 51 | courseDuration: Number(studentData.course_duration || 12), 52 | dueDate: new Date(new Date().setDate(new Date().getDate() + 30)).toISOString() // Default to 30 days from now 53 | }; 54 | 55 | const response = await fetch(`${API_BASE_URL}/api/students`, { 56 | method: 'POST', 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | ...authHeader() 60 | }, 61 | body: JSON.stringify(payload) 62 | }); 63 | return handleResponse(response); 64 | }, 65 | 66 | // Update student 67 | update: async (id, studentData) => { 68 | // Transform frontend data to match backend expectations 69 | const payload = { 70 | name: studentData.name, 71 | fathersName: studentData.fathers_name, 72 | phoneNo: studentData.phone_no, 73 | courseName: studentData.course_name, 74 | batchTime: studentData.batch_time, 75 | monthlyAmount: Number(studentData.monthly_fee), 76 | courseDuration: Number(studentData.course_duration || 12), 77 | dueDate: studentData.due_date 78 | }; 79 | 80 | const response = await fetch(`${API_BASE_URL}/api/students/${id}`, { 81 | method: 'PUT', 82 | headers: { 83 | 'Content-Type': 'application/json', 84 | ...authHeader() 85 | }, 86 | body: JSON.stringify(payload) 87 | }); 88 | return handleResponse(response); 89 | } 90 | }; 91 | 92 | // Fee API services 93 | export const FeeService = { 94 | // Get due fees 95 | getDueFees: async () => { 96 | const response = await fetch(`${API_BASE_URL}/api/fees/due`, { 97 | headers: { 98 | ...authHeader() 99 | } 100 | }); 101 | return handleResponse(response); 102 | }, 103 | 104 | // Update fee status 105 | updateStatus: async (id, status) => { 106 | const response = await fetch(`${API_BASE_URL}/api/fees/${id}/status`, { 107 | method: 'PUT', 108 | headers: { 109 | 'Content-Type': 'application/json', 110 | ...authHeader() 111 | }, 112 | body: JSON.stringify({ status }) 113 | }); 114 | return handleResponse(response); 115 | }, 116 | 117 | // Send fee reminder 118 | sendReminder: async (id) => { 119 | const response = await fetch(`${API_BASE_URL}/api/fees/send-reminder/${id}`, { 120 | method: 'POST', 121 | headers: { 122 | 'Content-Type': 'application/json', 123 | ...authHeader() 124 | } 125 | }); 126 | return handleResponse(response); 127 | } 128 | }; 129 | 130 | // Export API services 131 | export const ExportService = { 132 | // CSV and JSON exports only 133 | exportStudentsCSV: () => { 134 | window.open(`${API_BASE_URL}/api/export-v2/students/csv`, '_blank'); 135 | }, 136 | 137 | exportStudentsJSON: () => { 138 | window.open(`${API_BASE_URL}/api/export-v2/students/json`, '_blank'); 139 | }, 140 | 141 | exportNotificationsCSV: () => { 142 | window.open(`${API_BASE_URL}/api/export-v2/notifications/csv`, '_blank'); 143 | }, 144 | 145 | exportNotificationsJSON: () => { 146 | window.open(`${API_BASE_URL}/api/export-v2/notifications/json`, '_blank'); 147 | }, 148 | 149 | // Helper function to trigger download with proper error handling 150 | downloadFile: async (url, filename) => { 151 | try { 152 | const response = await fetch(url, { 153 | headers: { 154 | ...authHeader() 155 | } 156 | }); 157 | 158 | if (!response.ok) { 159 | throw new Error(`HTTP error! status: ${response.status}`); 160 | } 161 | 162 | const blob = await response.blob(); 163 | const downloadUrl = window.URL.createObjectURL(blob); 164 | const link = document.createElement('a'); 165 | link.href = downloadUrl; 166 | link.download = filename; 167 | document.body.appendChild(link); 168 | link.click(); 169 | link.remove(); 170 | window.URL.revokeObjectURL(downloadUrl); 171 | } catch (error) { 172 | console.error('Download failed:', error); 173 | throw error; 174 | } 175 | } 176 | }; 177 | 178 | // Dashboard statistics 179 | export const StatsService = { 180 | getDashboardStats: async () => { 181 | try { 182 | // Use the dedicated stats API endpoint we fixed 183 | const response = await fetch(`${API_BASE_URL}/api/stats`, { 184 | headers: { 185 | ...authHeader() 186 | } 187 | }); 188 | const data = await handleResponse(response); 189 | 190 | // Return the data from the backend 191 | return { 192 | totalStudents: data.totalStudents || 0, 193 | pendingFees: data.pendingFees || 0, 194 | totalRevenue: data.totalRevenue || 0, 195 | recentReminders: data.recentReminders || 0, 196 | dueStudentsTomorrow: data.dueStudentsTomorrow || [] 197 | }; 198 | } catch (error) { 199 | console.error("Error fetching dashboard stats:", error); 200 | throw error; 201 | } 202 | } 203 | }; 204 | 205 | export default { 206 | StudentService, 207 | FeeService, 208 | StatsService, 209 | ExportService 210 | }; 211 | -------------------------------------------------------------------------------- /backend/src/handlers/multi-export.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | // Configure AWS 4 | AWS.config.update({ 5 | region: process.env.AWS_REGION || 'YOUR_AWS_REGION' 6 | }); 7 | 8 | // Create DynamoDB DocumentClient 9 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 10 | 11 | // Helper function to get students data 12 | const getStudentsData = async () => { 13 | const params = { 14 | TableName: process.env.STUDENTS_TABLE || 'Students' 15 | }; 16 | 17 | const result = await dynamodb.scan(params).promise(); 18 | console.log(`Retrieved ${result.Items.length} students from database`); 19 | 20 | return result.Items.map(student => ({ 21 | id: student.id || '', 22 | registration_no: student.registration_no || '', 23 | name: student.name || '', 24 | fathers_name: student.fathers_name || '', 25 | phone_no: student.phone_no || student.phone || '', 26 | email: student.email || '', 27 | course_name: student.course_name || student.course || '', 28 | batch_time: student.batch_time || student.batchTime || '', 29 | created_at: student.created_at || '', 30 | monthly_fee: student.fees?.monthly_amount || '', 31 | fee_status: student.fees?.status || '', 32 | due_date: student.fees?.due_date || '', 33 | course_duration: student.course_duration || '' 34 | })); 35 | }; 36 | 37 | // Helper function to get notification logs data 38 | const getNotificationLogsData = async () => { 39 | const params = { 40 | TableName: process.env.NOTIFICATION_LOGS_TABLE || 'NotificationLogs' 41 | }; 42 | 43 | const result = await dynamodb.scan(params).promise(); 44 | console.log(`Retrieved ${result.Items.length} notification logs from database`); 45 | 46 | return result.Items.map(log => ({ 47 | id: log.id || '', 48 | student_id: log.student_id || '', 49 | student_name: log.student_name || '', 50 | phone_no: log.phone_no || '', 51 | status: log.status || '', 52 | message: log.message || '', 53 | created_at: log.created_at || '' 54 | })); 55 | }; 56 | 57 | // CSV Export for Students 58 | exports.exportStudentsCSV = async (req, res) => { 59 | try { 60 | const students = await getStudentsData(); 61 | 62 | // Create CSV content 63 | const headers = [ 64 | 'ID', 'Registration No', 'Name', "Father's Name", 'Phone', 'Email', 65 | 'Course', 'Batch Time', 'Joining Date', 'Monthly Fee', 'Fee Status', 'Due Date', 'Course Duration' 66 | ]; 67 | 68 | let csvContent = headers.join(',') + '\n'; 69 | 70 | students.forEach(student => { 71 | const row = [ 72 | `"${student.id}"`, 73 | `"${student.registration_no}"`, 74 | `"${student.name}"`, 75 | `"${student.fathers_name}"`, 76 | `"${student.phone_no}"`, 77 | `"${student.email}"`, 78 | `"${student.course_name}"`, 79 | `"${student.batch_time}"`, 80 | `"${student.created_at}"`, 81 | `"${student.monthly_fee}"`, 82 | `"${student.fee_status}"`, 83 | `"${student.due_date}"`, 84 | `"${student.course_duration}"` 85 | ]; 86 | csvContent += row.join(',') + '\n'; 87 | }); 88 | 89 | res.setHeader('Content-Type', 'text/csv'); 90 | res.setHeader('Content-Disposition', 'attachment; filename=students.csv'); 91 | res.setHeader('Access-Control-Allow-Origin', '*'); 92 | 93 | res.send(csvContent); 94 | console.log("CSV file sent successfully"); 95 | } catch (error) { 96 | console.error('Error exporting students as CSV:', error); 97 | res.status(500).json({ 98 | success: false, 99 | message: 'Failed to export students as CSV', 100 | error: error.message 101 | }); 102 | } 103 | }; 104 | 105 | // JSON Export for Students 106 | exports.exportStudentsJSON = async (req, res) => { 107 | try { 108 | const students = await getStudentsData(); 109 | 110 | res.setHeader('Content-Type', 'application/json'); 111 | res.setHeader('Content-Disposition', 'attachment; filename=students.json'); 112 | res.setHeader('Access-Control-Allow-Origin', '*'); 113 | 114 | res.json({ 115 | export_date: new Date().toISOString(), 116 | total_count: students.length, 117 | students: students 118 | }); 119 | 120 | console.log("JSON file sent successfully"); 121 | } catch (error) { 122 | console.error('Error exporting students as JSON:', error); 123 | res.status(500).json({ 124 | success: false, 125 | message: 'Failed to export students as JSON', 126 | error: error.message 127 | }); 128 | } 129 | }; 130 | 131 | // CSV Export for Notifications 132 | exports.exportNotificationsCSV = async (req, res) => { 133 | try { 134 | const logs = await getNotificationLogsData(); 135 | 136 | // Create CSV content 137 | const headers = ['ID', 'Student ID', 'Student Name', 'Phone Number', 'Status', 'Message', 'Sent At']; 138 | 139 | let csvContent = headers.join(',') + '\n'; 140 | 141 | logs.forEach(log => { 142 | const row = [ 143 | `"${log.id}"`, 144 | `"${log.student_id}"`, 145 | `"${log.student_name}"`, 146 | `"${log.phone_no}"`, 147 | `"${log.status}"`, 148 | `"${(log.message || '').replace(/"/g, '""')}"`, // Escape quotes in message 149 | `"${log.created_at}"` 150 | ]; 151 | csvContent += row.join(',') + '\n'; 152 | }); 153 | 154 | res.setHeader('Content-Type', 'text/csv'); 155 | res.setHeader('Content-Disposition', 'attachment; filename=notification_logs.csv'); 156 | res.setHeader('Access-Control-Allow-Origin', '*'); 157 | 158 | res.send(csvContent); 159 | console.log("Notifications CSV file sent successfully"); 160 | } catch (error) { 161 | console.error('Error exporting notifications as CSV:', error); 162 | res.status(500).json({ 163 | success: false, 164 | message: 'Failed to export notifications as CSV', 165 | error: error.message 166 | }); 167 | } 168 | }; 169 | 170 | // JSON Export for Notifications 171 | exports.exportNotificationsJSON = async (req, res) => { 172 | try { 173 | const logs = await getNotificationLogsData(); 174 | 175 | res.setHeader('Content-Type', 'application/json'); 176 | res.setHeader('Content-Disposition', 'attachment; filename=notification_logs.json'); 177 | res.setHeader('Access-Control-Allow-Origin', '*'); 178 | 179 | res.json({ 180 | export_date: new Date().toISOString(), 181 | total_count: logs.length, 182 | notification_logs: logs 183 | }); 184 | 185 | console.log("Notifications JSON file sent successfully"); 186 | } catch (error) { 187 | console.error('Error exporting notifications as JSON:', error); 188 | res.status(500).json({ 189 | success: false, 190 | message: 'Failed to export notifications as JSON', 191 | error: error.message 192 | }); 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/pages/FeeManagement.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Table, Button, Form, InputGroup, Row, Col, Card, Alert, Spinner } from 'react-bootstrap'; 3 | import { StudentService, FeeService } from '../services/api'; 4 | import { toast } from 'react-toastify'; 5 | 6 | const FeeManagement = () => { 7 | const [students, setStudents] = useState([]); 8 | const [loading, setLoading] = useState(true); 9 | const [error, setError] = useState(null); 10 | const [filter, setFilter] = useState('all'); 11 | 12 | useEffect(() => { 13 | fetchFeeData(); 14 | }, []); 15 | 16 | const fetchFeeData = async () => { 17 | try { 18 | setLoading(true); 19 | 20 | // Get all students to show their fee information 21 | const data = await StudentService.getAll(); 22 | setStudents(data.students || []); 23 | } catch (err) { 24 | console.error('Error fetching fee data:', err); 25 | setError('Failed to load fee data. Please try again.'); 26 | } finally { 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | // Transform students data to fee records for the table 32 | const fees = students.map(student => ({ 33 | id: student.id, 34 | studentName: student.name, 35 | amount: student.fees?.monthly_amount || 0, 36 | dueDate: student.fees?.due_date || null, 37 | status: student.fees?.status || 'pending', 38 | paidDate: student.fees?.last_paid || null 39 | })); 40 | 41 | const filteredFees = filter === 'all' 42 | ? fees 43 | : fees.filter(fee => fee.status === filter); 44 | 45 | const markAsPaid = async (feeId) => { 46 | try { 47 | await FeeService.updateStatus(feeId, 'paid'); 48 | 49 | // Update the local state 50 | setStudents(students.map(student => { 51 | if (student.id === feeId) { 52 | return { 53 | ...student, 54 | fees: { 55 | ...student.fees, 56 | status: 'paid', 57 | last_paid: new Date().toISOString() 58 | } 59 | }; 60 | } 61 | return student; 62 | })); 63 | 64 | toast.success('Fee marked as paid successfully'); 65 | } catch (error) { 66 | console.error('Error marking fee as paid:', error); 67 | toast.error(`Failed to update fee status: ${error.message}`); 68 | } 69 | }; 70 | 71 | const formatDate = (dateString) => { 72 | if (!dateString) return '-'; 73 | const date = new Date(dateString); 74 | return date.toLocaleDateString('en-IN'); 75 | }; 76 | return ( 77 |
78 |

Fee Management

79 | 80 | {error && {error}} 81 | 82 | 83 | 84 | 85 | 86 | Total Due 87 | 88 | ₹{fees 89 | .filter(fee => fee.status !== 'paid') 90 | .reduce((total, fee) => total + fee.amount, 0) 91 | .toLocaleString('en-IN')} 92 | 93 | 94 | 95 | 96 | 97 | 98 | Filter by Status 99 | setFilter(e.target.value)} 102 | > 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {loading ? ( 113 |
114 | 115 |

Loading fee data...

116 |
117 | ) : ( 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | {filteredFees.length > 0 ? ( 132 | filteredFees.map(fee => ( 133 | 134 | 135 | 136 | 137 | 138 | 143 | 144 | 172 | 173 | )) 174 | ) : ( 175 | 176 | 179 | )} 180 | 181 |
Student IDStudent NameAmountDue DateStatusPaid DateActions
{fee.id.substring(0, 8)}...{fee.studentName}₹{fee.amount.toLocaleString('en-IN')}{formatDate(fee.dueDate)} 139 | 140 | {fee.status.toUpperCase()} 141 | 142 | {formatDate(fee.paidDate)} 145 | 153 | {' '} 154 | 171 |
177 | {filter !== 'all' ? 'No fees match the selected filter' : 'No fee records found'} 178 |
182 | )} 183 |
184 | ); 185 | }; 186 | 187 | export default FeeManagement; -------------------------------------------------------------------------------- /backend/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const serverless = require('serverless-http'); 3 | const cors = require('cors'); 4 | const cookieParser = require('cookie-parser'); 5 | const helmet = require('helmet'); 6 | const studentRoutes = require('./handlers/student'); 7 | const feeRoutes = require('./handlers/fees'); 8 | const notificationRoutes = require('./handlers/notifications'); 9 | const reminderRoutes = require('./handlers/reminders'); 10 | const statsRoutes = require('./handlers/stats'); 11 | const exportRoutes = require('./handlers/export'); 12 | const multiExportHandler = require('./handlers/multi-export'); 13 | const authRoutes = require('./handlers/auth'); 14 | const authMiddleware = require('./utils/auth-middleware'); 15 | const { createTableIfNotExists } = require('./utils/database'); 16 | const reminderJob = require('./jobs/reminderJob'); 17 | const axios = require('axios'); 18 | 19 | // Validate required environment variables 20 | const requiredEnvVars = ['AWS_REGION', 'AISENSY_API_KEY', 'AISENSY_PARTNER_ID']; 21 | const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); 22 | 23 | if (missingVars.length > 0) { 24 | console.warn(`Missing environment variables: ${missingVars.join(', ')}`); 25 | console.warn('Some features may not work correctly!'); 26 | } 27 | 28 | const app = express(); 29 | 30 | // Middleware 31 | app.use(cors({ 32 | origin: process.env.CORS_ORIGIN || '*', // Restrict in production 33 | credentials: true, // Allow cookies 34 | methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 35 | allowedHeaders: ['Content-Type', 'Authorization'] 36 | })); 37 | app.use(helmet()); // Security headers 38 | app.use(express.json({ limit: '1mb' })); // Limit payload size 39 | app.use(express.urlencoded({ extended: true })); 40 | app.use(cookieParser()); // Parse cookies for auth 41 | 42 | // Create DynamoDB table if it doesn't exist 43 | createTableIfNotExists() 44 | .then(() => console.log('DynamoDB table checked/created successfully')) 45 | .catch(err => console.error('Error with DynamoDB table setup:', err)); 46 | 47 | // Authentication Routes (unprotected) 48 | app.use('/api/auth', authRoutes); 49 | 50 | // Protected API Routes (require authentication) 51 | app.use('/api/students', authMiddleware, studentRoutes); 52 | app.use('/api/fees', authMiddleware, feeRoutes); 53 | app.use('/api/notifications', authMiddleware, notificationRoutes); 54 | app.use('/api/reminders', authMiddleware, reminderRoutes); 55 | app.use('/api/stats', authMiddleware, statsRoutes); 56 | app.use('/api/export', authMiddleware, exportRoutes); 57 | 58 | // CSV and JSON export endpoints only (protected) 59 | app.get('/api/export-v2/students/csv', authMiddleware, multiExportHandler.exportStudentsCSV); 60 | app.get('/api/export-v2/students/json', authMiddleware, multiExportHandler.exportStudentsJSON); 61 | app.get('/api/export-v2/notifications/csv', authMiddleware, multiExportHandler.exportNotificationsCSV); 62 | app.get('/api/export-v2/notifications/json', authMiddleware, multiExportHandler.exportNotificationsJSON); 63 | 64 | // Health check 65 | app.get('/health', (req, res) => { 66 | res.json({ status: 'OK', timestamp: new Date().toISOString() }); 67 | }); 68 | 69 | // Enhanced logging middleware 70 | app.use((req, res, next) => { 71 | console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - Body:`, 72 | req.method === 'GET' ? '(GET request)' : JSON.stringify(req.body).substring(0, 200)); 73 | 74 | // Capture original send to log responses 75 | const originalSend = res.send; 76 | res.send = function(data) { 77 | console.log(`[${new Date().toISOString()}] Response ${res.statusCode}:`, 78 | data ? data.toString().substring(0, 200) + '...' : 'No data'); 79 | return originalSend.apply(res, arguments); 80 | }; 81 | 82 | next(); 83 | }); 84 | app.get('/test', async (req, res) => { 85 | try { 86 | const axios = require('axios'); 87 | const phoneNumber = 'YOUR_TEST_PHONE'; 88 | const message = 'Testing from Production - Your fee of Rs.5000 is due on 30/07/2025. Pay now: YOUR_PAYMENT_URL'; 89 | const apiKey = process.env.FAST2SMS_API_KEY; 90 | 91 | console.log('TEST ENDPOINT: Sending SMS to', phoneNumber); 92 | 93 | if (!apiKey) { 94 | console.error('FAST2SMS_API_KEY not set in environment'); 95 | return res.status(500).json({ 96 | success: false, 97 | error: 'SMS service configuration error' 98 | }); 99 | } 100 | 101 | // Make direct API call to Fast2SMS 102 | const response = await axios.get('https://www.fast2sms.com/dev/bulkV2', { 103 | params: { 104 | authorization: apiKey, 105 | route: 'q', 106 | message: message, 107 | language: 'english', 108 | flash: '0', 109 | numbers: phoneNumber 110 | } 111 | }); 112 | 113 | console.log('Fast2SMS API Response:', JSON.stringify(response.data)); 114 | 115 | return res.json({ 116 | success: true, 117 | message: 'SMS sent successfully', 118 | result: { 119 | phoneNumber: phoneNumber, 120 | message: message, 121 | status: response.data.return ? 'sent' : 'failed', 122 | timestamp: new Date().toISOString(), 123 | fast2sms: response.data 124 | } 125 | }); 126 | } catch (err) { 127 | console.error('TEST ENDPOINT ERROR:', err); 128 | 129 | // Get more details about the error 130 | let errorDetails = { message: err.message }; 131 | if (err.response) { 132 | errorDetails.status = err.response.status; 133 | errorDetails.data = err.response.data; 134 | } 135 | 136 | res.status(500).json({ 137 | success: false, 138 | error: errorDetails 139 | }); 140 | } 141 | }); 142 | // Initialize jobs (add this near the bottom of your file, before app.listen) 143 | if (process.env.NODE_ENV === 'production') { 144 | // Only run scheduled jobs in production 145 | reminderJob.init(); 146 | console.log('Scheduled jobs initialized'); 147 | } 148 | 149 | // SMS sending function 150 | async function sendSMS({ to, message }) { 151 | const apiKey = process.env.FAST2SMS_API_KEY; 152 | const url = 'https://www.fast2sms.com/dev/bulkV2'; 153 | 154 | const params = { 155 | authorization: apiKey, 156 | route: 'q', 157 | message, 158 | numbers: to, 159 | flash: '0' 160 | }; 161 | 162 | try { 163 | const response = await axios.get(url, { params }); 164 | return response.data; 165 | } catch (error) { 166 | throw new Error(error.response?.data?.message || error.message); 167 | } 168 | } 169 | 170 | // Export for Lambda 171 | module.exports.handler = serverless(app); 172 | 173 | // Also export the Express app for local server 174 | module.exports.app = app; 175 | 176 | // Also export the Express app for local server 177 | module.exports.app = app; -------------------------------------------------------------------------------- /backend/src/utils/notification-v2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Notification System v4 - Fast2SMS 3 | * 4 | * Enhanced notification system using Fast2SMS for SMS delivery 5 | * Simple SMS solution with no DLT requirements (₹5 per SMS) 6 | */ 7 | 8 | const fast2SMS = require('./fast2sms'); 9 | const NotificationLog = require('./simple-logs'); 10 | 11 | // Provider preference - Fast2SMS is now the primary provider 12 | const PRIMARY_PROVIDER = process.env.NOTIFICATION_PROVIDER || 'fast2sms'; 13 | 14 | /** 15 | * Send an SMS notification using Fast2SMS 16 | * 17 | * @param {string} phoneNumber - The recipient's phone number 18 | * @param {string} message - The SMS message content 19 | * @param {object} metadata - Additional metadata for logging 20 | * @returns {Promise} Result of the send operation 21 | */ 22 | async function sendSMSNotification(phoneNumber, message, metadata = {}) { 23 | try { 24 | console.log(`Sending SMS notification via Fast2SMS:`, { 25 | phone: phoneNumber, 26 | messageLength: message.length, 27 | provider: 'fast2sms' 28 | }); 29 | 30 | // Send SMS via Fast2SMS 31 | const result = await fast2SMS.sendSMS(phoneNumber, message, metadata); 32 | 33 | return result; 34 | } catch (error) { 35 | console.error('Error in SMS notification system:', error.message); 36 | return { 37 | success: false, 38 | error: error.message, 39 | provider: 'fast2sms' 40 | }; 41 | } 42 | } 43 | 44 | /** 45 | * Send a fee reminder SMS 46 | * 47 | * @param {object} student - The student object 48 | * @param {object} feeData - Fee reminder data (optional) 49 | * @returns {Promise} Result of the send operation 50 | */ 51 | async function sendFeeReminder(student, feeData = {}) { 52 | if (!student || !student.phone) { 53 | console.error('Cannot send fee reminder: Invalid student data or missing phone number'); 54 | return { success: false, error: 'Invalid student data' }; 55 | } 56 | 57 | try { 58 | console.log(`Sending fee reminder to: ${student.phone}`); 59 | console.log('Student data:', { 60 | name: student.name, 61 | course: student.course, 62 | amount: student.pendingAmount, 63 | dueDate: student.dueDate 64 | }); 65 | 66 | // Use the Fast2SMS service to send fee reminder 67 | const result = await fast2SMS.sendFeeReminder(student, feeData); 68 | 69 | return result; 70 | } catch (error) { 71 | console.error('Error sending fee reminder:', error.message); 72 | return { 73 | success: false, 74 | error: error.message, 75 | provider: 'fast2sms' 76 | }; 77 | } 78 | } 79 | 80 | /** 81 | * Send family reminder SMS 82 | * 83 | * @param {object} student - The student object 84 | * @param {string} familyPhone - Family member's phone number 85 | * @param {object} feeData - Fee reminder data (optional) 86 | * @returns {Promise} Result of the send operation 87 | */ 88 | async function sendFamilyReminder(student, familyPhone, feeData = {}) { 89 | if (!student || !familyPhone) { 90 | console.error('Cannot send family reminder: Invalid data'); 91 | return { success: false, error: 'Invalid data' }; 92 | } 93 | 94 | try { 95 | console.log(`Sending family reminder to: ${familyPhone} for student: ${student.name}`); 96 | 97 | // Use the Fast2SMS service to send family reminder 98 | const result = await fast2SMS.sendFamilyReminder(student, familyPhone, feeData); 99 | 100 | return result; 101 | } catch (error) { 102 | console.error('Error sending family reminder:', error.message); 103 | return { 104 | success: false, 105 | error: error.message, 106 | provider: 'fast2sms' 107 | }; 108 | } 109 | } 110 | 111 | /** 112 | * Send bulk notifications to multiple students 113 | * 114 | * @param {Array} students - Array of student objects 115 | * @param {object} feeData - Common fee data (optional) 116 | * @returns {Promise} Summary of bulk send operation 117 | */ 118 | async function sendBulkFeeReminders(students, feeData = {}) { 119 | if (!Array.isArray(students) || students.length === 0) { 120 | return { success: false, error: 'Invalid students array' }; 121 | } 122 | 123 | try { 124 | console.log(`Sending bulk fee reminders to ${students.length} students...`); 125 | 126 | // Prepare recipients for bulk SMS 127 | const recipients = students 128 | .filter(student => student.phone) // Only include students with phone numbers 129 | .map(student => ({ 130 | phone: student.phone, 131 | message: fast2SMS.generateFeeReminderMessage(student, feeData), 132 | metadata: { 133 | studentId: student.id, 134 | studentName: student.name, 135 | messageType: 'fee_reminder', 136 | amount: student.pendingAmount, 137 | dueDate: student.dueDate, 138 | course: student.course 139 | } 140 | })); 141 | 142 | if (recipients.length === 0) { 143 | return { success: false, error: 'No students with valid phone numbers' }; 144 | } 145 | 146 | // Send bulk SMS 147 | const results = await fast2SMS.sendBulkSMS(recipients); 148 | 149 | // Calculate summary 150 | const successful = results.filter(r => r.success).length; 151 | const failed = results.filter(r => !r.success).length; 152 | 153 | console.log(`Bulk SMS completed. Success: ${successful}, Failed: ${failed}`); 154 | 155 | return { 156 | success: true, 157 | summary: { 158 | total: recipients.length, 159 | successful, 160 | failed, 161 | details: results 162 | } 163 | }; 164 | } catch (error) { 165 | console.error('Error sending bulk fee reminders:', error.message); 166 | return { 167 | success: false, 168 | error: error.message 169 | }; 170 | } 171 | } 172 | 173 | /** 174 | * Send custom SMS message 175 | * 176 | * @param {string} phoneNumber - Recipient phone number 177 | * @param {string} message - Custom message content 178 | * @param {object} metadata - Additional metadata 179 | * @returns {Promise} Result of the send operation 180 | */ 181 | async function sendCustomSMS(phoneNumber, message, metadata = {}) { 182 | return sendSMSNotification(phoneNumber, message, metadata); 183 | } 184 | 185 | module.exports = { 186 | sendSMSNotification, 187 | sendFeeReminder, 188 | sendFamilyReminder, 189 | sendBulkFeeReminders, 190 | sendCustomSMS, 191 | // Legacy compatibility - map old function names to new ones 192 | sendWhatsAppNotification: sendSMSNotification, // For backward compatibility 193 | // Export Fast2SMS service for direct access if needed 194 | fast2SMS 195 | }; 196 | -------------------------------------------------------------------------------- /backend/src/handlers/reminders.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API Handler for manually triggering fee reminders 3 | * This is useful for testing and admin-initiated batch reminders 4 | */ 5 | const express = require('express'); 6 | const router = express.Router(); 7 | const { handler: feeReminderHandler } = require('../lambda/feeReminder'); 8 | const Student = require('../models/Student'); 9 | const { sendFeeReminder } = require('../utils/notification-v2'); 10 | const NotificationLog = require('../utils/notification-logs'); 11 | 12 | // POST /api/reminders/send-batch - Manually trigger batch reminders 13 | router.post('/send-batch', async (req, res) => { 14 | try { 15 | console.log('Manually triggering batch fee reminders'); 16 | 17 | // Execute the same handler as the scheduled function 18 | const result = await feeReminderHandler({ 19 | source: 'manual-api-trigger', 20 | timestamp: new Date().toISOString() 21 | }); 22 | 23 | return res.status(200).json({ 24 | success: true, 25 | message: 'Batch reminders triggered successfully', 26 | result: JSON.parse(result.body) 27 | }); 28 | } catch (error) { 29 | console.error('Error triggering batch reminders:', error); 30 | return res.status(500).json({ 31 | success: false, 32 | message: 'Error triggering batch reminders', 33 | error: error.message 34 | }); 35 | } 36 | }); 37 | 38 | // POST /api/reminders/filter - Send reminders with custom filter 39 | router.post('/filter', async (req, res) => { 40 | try { 41 | const { dueInDays, courseName } = req.body; 42 | 43 | if (!dueInDays && !courseName) { 44 | return res.status(400).json({ 45 | success: false, 46 | message: 'Filter criteria required (dueInDays or courseName)' 47 | }); 48 | } 49 | 50 | // Get students with pending fees 51 | const students = await Student.getStudentsWithDueFees(); 52 | let filteredStudents = [...students]; 53 | 54 | // Apply course filter if specified 55 | if (courseName) { 56 | filteredStudents = filteredStudents.filter(student => 57 | student.course_name.toLowerCase() === courseName.toLowerCase() 58 | ); 59 | } 60 | 61 | // Apply due date filter if specified 62 | if (dueInDays) { 63 | const today = new Date(); 64 | const targetDate = new Date(today); 65 | targetDate.setDate(today.getDate() + parseInt(dueInDays)); 66 | 67 | filteredStudents = filteredStudents.filter(student => { 68 | if (!student.fees || !student.fees.due_date) return false; 69 | const dueDate = new Date(student.fees.due_date); 70 | return dueDate.toDateString() === targetDate.toDateString(); 71 | }); 72 | } 73 | 74 | console.log(`Found ${filteredStudents.length} students matching filter criteria`); 75 | 76 | // Send notifications to filtered students 77 | let successCount = 0; 78 | let failCount = 0; 79 | let notifications = []; 80 | 81 | for (const student of filteredStudents) { 82 | try { 83 | const dueDate = new Date(student.fees.due_date).toLocaleDateString('en-IN'); 84 | 85 | // Send notification 86 | const result = await notifyStudent( 87 | student.phone_no, 88 | student.name, 89 | student.fees.monthly_amount, 90 | student.course_name, 91 | dueDate 92 | ); 93 | 94 | // Log the notification 95 | await NotificationLog.create({ 96 | studentId: student.id, 97 | studentName: student.name, 98 | phoneNumber: student.phone_no, 99 | status: 'sent', 100 | type: 'filtered_fee_reminder', 101 | message: `Fee reminder for ${student.course_name} - Due on ${dueDate}`, 102 | templateName: 'fee_reminder_sms', 103 | metadata: { 104 | amount: student.fees.monthly_amount, 105 | course: student.course_name, 106 | dueDate: dueDate, 107 | filter: { dueInDays, courseName } 108 | }, 109 | responseData: result 110 | }); 111 | 112 | notifications.push({ 113 | studentId: student.id, 114 | studentName: student.name, 115 | status: 'sent' 116 | }); 117 | 118 | successCount++; 119 | } catch (error) { 120 | notifications.push({ 121 | studentId: student.id, 122 | studentName: student.name, 123 | status: 'failed', 124 | error: error.message 125 | }); 126 | 127 | failCount++; 128 | } 129 | 130 | // Add a small delay between notifications 131 | await new Promise(resolve => setTimeout(resolve, 500)); 132 | } 133 | 134 | return res.status(200).json({ 135 | success: true, 136 | message: 'Filtered reminders sent', 137 | totalStudents: filteredStudents.length, 138 | successCount: successCount, 139 | failCount: failCount, 140 | notifications: notifications 141 | }); 142 | } catch (error) { 143 | console.error('Error sending filtered reminders:', error); 144 | return res.status(500).json({ 145 | success: false, 146 | message: 'Error sending filtered reminders', 147 | error: error.message 148 | }); 149 | } 150 | }); 151 | 152 | // GET /api/reminders/notification-history - Get notification history 153 | router.get('/notification-history', async (req, res) => { 154 | try { 155 | const { studentId, startDate, endDate } = req.query; 156 | 157 | let logs = []; 158 | 159 | if (studentId) { 160 | logs = await NotificationLog.getByStudentId(studentId); 161 | } else if (startDate) { 162 | logs = await NotificationLog.getByDateRange(startDate, endDate); 163 | } else { 164 | return res.status(400).json({ 165 | success: false, 166 | message: 'Filter criteria required (studentId or dateRange)' 167 | }); 168 | } 169 | 170 | return res.status(200).json({ 171 | success: true, 172 | count: logs.length, 173 | logs: logs 174 | }); 175 | } catch (error) { 176 | console.error('Error fetching notification history:', error); 177 | return res.status(500).json({ 178 | success: false, 179 | message: 'Error fetching notification history', 180 | error: error.message 181 | }); 182 | } 183 | }); 184 | 185 | module.exports = router; 186 | -------------------------------------------------------------------------------- /backend/serverless.yml: -------------------------------------------------------------------------------- 1 | service: fee-management-system- 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs16.x 6 | stage: ${opt:stage, ''} 7 | region: YOUR_AWS_REGION 8 | deploymentBucket: 9 | name: # Specify existing bucket name here 10 | environment: 11 | STUDENTS_TABLE: Students-${self:provider.stage} 12 | FEES_TABLE: Fees-${self:provider.stage} 13 | NOTIFICATION_LOGS_TABLE: NotificationLogs-${self:provider.stage} 14 | # Production environment variables 15 | NODE_ENV: 'production' 16 | # Security variables for authentication 17 | JWT_SECRET: ${env:JWT_SECRET, 'YOUR_JWT_SECRET'} 18 | ADMIN_EMAIL: ${env:ADMIN_EMAIL, 'YOUR_ADMIN_EMAIL'} 19 | # Updated password hash from generate-admin-password.js 20 | ADMIN_PASSWORD_HASH: ${env:ADMIN_PASSWORD_HASH, 'YOUR_ADMIN_PASSWORD_HASH'} 21 | PASSWORD_SALT: ${env:PASSWORD_SALT, 'YOUR_PASSWORD_SALT'} 22 | # CORS configuration for production 23 | CORS_ORIGIN: 'YOUR_FRONTEND_URL' 24 | 25 | # Fast2SMS Configuration (New SMS provider) 26 | NOTIFICATION_PROVIDER: ${env:NOTIFICATION_PROVIDER, 'fast2sms'} 27 | SMS_ENABLED: ${env:SMS_ENABLED, 'true'} 28 | FAST2SMS_API_KEY: ${env:FAST2SMS_API_KEY, 'YOUR_FAST2SMS_API_KEY'} 29 | # Removed reserved AWS_REGION environment variable 30 | 31 | # Institute Configuration 32 | INSTITUTE_NAME: ${env:INSTITUTE_NAME, 'YOUR_NAME'} 33 | SUPPORT_PHONE: ${env:SUPPORT_PHONE, 'YOUR_NO'} 34 | 35 | # Add these IAM permissions 36 | iam: 37 | role: 38 | statements: 39 | - Effect: Allow 40 | Action: 41 | - dynamodb:ListTables 42 | Resource: "*" 43 | - Effect: Allow 44 | Action: 45 | - dynamodb:Query 46 | - dynamodb:Scan 47 | - dynamodb:GetItem 48 | - dynamodb:PutItem 49 | - dynamodb:UpdateItem 50 | - dynamodb:DeleteItem 51 | Resource: 52 | # Template-based table names (from environment variables) 53 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.STUDENTS_TABLE}" 54 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.STUDENTS_TABLE}/index/*" 55 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.FEES_TABLE}" 56 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.FEES_TABLE}/index/*" 57 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.NOTIFICATION_LOGS_TABLE}" 58 | - "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.NOTIFICATION_LOGS_TABLE}/index/*" 59 | # Direct table names (from Resources section) 60 | - "arn:aws:dynamodb:${self:provider.region}:*:table/Students-${self:provider.stage}" 61 | - "arn:aws:dynamodb:${self:provider.region}:*:table/Students-${self:provider.stage}/index/*" 62 | - "arn:aws:dynamodb:${self:provider.region}:*:table/NotificationLogs-${self:provider.stage}" 63 | - "arn:aws:dynamodb:${self:provider.region}:*:table/NotificationLogs-${self:provider.stage}/index/*" 64 | 65 | functions: 66 | api: 67 | handler: src/app.handler 68 | events: 69 | - http: 70 | path: /{proxy+} 71 | method: ANY 72 | cors: 73 | origin: ${self:provider.environment.CORS_ORIGIN} # Restricted to your cors origin in production 74 | headers: 75 | - Content-Type 76 | - X-Amz-Date 77 | - Authorization 78 | - X-Api-Key 79 | - X-Amz-Security-Token 80 | - X-Amz-User-Agent 81 | allowCredentials: true # Allow credentials for auth tokens 82 | - http: 83 | path: / 84 | method: ANY 85 | cors: 86 | origin: ${self:provider.environment.CORS_ORIGIN} 87 | allowCredentials: true 88 | 89 | # Scheduled fee reminder (AWS SNS SMS) 90 | dailyFeeReminder: 91 | handler: src/lambda/feeReminder.handler 92 | events: 93 | - schedule: 94 | name: DailyFeeReminderSchedule-${self:provider.stage} 95 | description: 'Checks for upcoming fee due dates and sends SMS reminders daily at 7 AM IST' 96 | rate: cron(0 1 30 * ? *) # Run daily at 6:30 AM IST (1:00 AM UTC) 97 | enabled: true 98 | 99 | # Legacy daily notifications (keeping for backward compatibility but disabled) 100 | dailyNotifications: 101 | handler: src/handlers/fees.checkDueFees 102 | events: 103 | - schedule: 104 | rate: cron(0 2 30 * ? *) # Run daily at 7:30 AM IST (2:00 AM UTC) 105 | enabled: false # Disabled as we're using the new implementation 106 | 107 | # Manual notification trigger 108 | sendTestNotification: 109 | handler: src/handlers/notifications.sendTestMessage 110 | 111 | resources: 112 | Resources: 113 | StudentsTable: 114 | Type: AWS::DynamoDB::Table 115 | Properties: 116 | TableName: Students-${self:provider.stage} 117 | BillingMode: PAY_PER_REQUEST 118 | AttributeDefinitions: 119 | - AttributeName: id 120 | AttributeType: S 121 | - AttributeName: registration_no 122 | AttributeType: S 123 | - AttributeName: course_name 124 | AttributeType: S 125 | KeySchema: 126 | - AttributeName: id 127 | KeyType: HASH 128 | GlobalSecondaryIndexes: 129 | - IndexName: RegistrationIndex 130 | KeySchema: 131 | - AttributeName: registration_no 132 | KeyType: HASH 133 | Projection: 134 | ProjectionType: ALL 135 | - IndexName: CourseIndex 136 | KeySchema: 137 | - AttributeName: course_name 138 | KeyType: HASH 139 | Projection: 140 | ProjectionType: ALL 141 | 142 | NotificationLogsTable: 143 | Type: AWS::DynamoDB::Table 144 | Properties: 145 | TableName: NotificationLogs-${self:provider.stage} 146 | BillingMode: PAY_PER_REQUEST 147 | AttributeDefinitions: 148 | - AttributeName: id 149 | AttributeType: S 150 | - AttributeName: student_id 151 | AttributeType: S 152 | - AttributeName: created_at 153 | AttributeType: S 154 | KeySchema: 155 | - AttributeName: id 156 | KeyType: HASH 157 | GlobalSecondaryIndexes: 158 | - IndexName: StudentIdIndex 159 | KeySchema: 160 | - AttributeName: student_id 161 | KeyType: HASH 162 | - AttributeName: created_at 163 | KeyType: RANGE 164 | Projection: 165 | ProjectionType: ALL 166 | 167 | # Stage-specific settings 168 | custom: 169 | stages: 170 | dev: 171 | logRetentionInDays: 7 172 | cors: 'http://localhost:3000' # For local development 173 | prod: 174 | logRetentionInDays: 30 175 | cors: '' 176 | 177 | # Optimize package size for faster deployments 178 | package: 179 | individually: true 180 | excludeDevDependencies: true 181 | exclude: 182 | - '**/*.test.js' 183 | - '**/test-*.js' 184 | - 'node_modules/aws-sdk/**' # AWS SDK is already available in Lambda 185 | - '*.xlsx' 186 | - '.env*' 187 | - '*.md' 188 | 189 | -------------------------------------------------------------------------------- /backend/src/handlers/notifications.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { sendCustomSMS, sendBulkFeeReminders } = require('../utils/notification-v2'); 4 | const Student = require('../models/Student'); 5 | 6 | // POST /api/notifications/test - Test SMS notification 7 | router.post('/test', async (req, res) => { 8 | const { phoneNumber, message } = req.body; 9 | 10 | if (!phoneNumber || !message) { 11 | return res.status(400).json({ 12 | success: false, 13 | message: 'Phone number and message are required' 14 | }); 15 | } 16 | 17 | try { 18 | const result = await sendCustomSMS(phoneNumber, message, { 19 | messageType: 'test', 20 | source: 'api_test' 21 | }); 22 | 23 | res.status(200).json({ 24 | success: true, 25 | message: 'Test SMS notification sent', 26 | result: result 27 | }); 28 | } catch (error) { 29 | res.status(500).json({ 30 | success: false, 31 | message: error.message 32 | }); 33 | } 34 | }); 35 | 36 | // POST /api/notifications/broadcast - Send SMS to all students 37 | router.post('/broadcast', async (req, res) => { 38 | const { message, courseFilter } = req.body; 39 | 40 | if (!message) { 41 | return res.status(400).json({ 42 | success: false, 43 | message: 'Message is required' 44 | }); 45 | } 46 | 47 | try { 48 | let students; 49 | 50 | if (courseFilter) { 51 | students = await Student.getByCourse(courseFilter); 52 | } else { 53 | students = await Student.findAll(); 54 | } 55 | 56 | if (!students || students.length === 0) { 57 | return res.status(404).json({ 58 | success: false, 59 | message: 'No students found' 60 | }); 61 | } 62 | 63 | console.log(`Broadcasting SMS to ${students.length} students`); 64 | 65 | // Prepare students for bulk SMS 66 | const studentsWithPhone = students.filter(student => student.phone || student.phone_no); 67 | 68 | if (studentsWithPhone.length === 0) { 69 | return res.status(400).json({ 70 | success: false, 71 | message: 'No students with valid phone numbers found' 72 | }); 73 | } 74 | 75 | // Create recipients array for bulk SMS 76 | const recipients = studentsWithPhone.map(student => ({ 77 | phone: student.phone || student.phone_no, 78 | message: message, 79 | metadata: { 80 | studentId: student.id, 81 | studentName: student.name, 82 | messageType: 'broadcast', 83 | source: 'api_broadcast' 84 | } 85 | })); 86 | 87 | // Send bulk SMS using AWS SNS 88 | const awsSNS = require('../utils/aws-sns'); 89 | const result = await awsSNS.sendBulkSMS(recipients); 90 | 91 | // Calculate summary 92 | const successful = result.filter(r => r.success).length; 93 | const failed = result.filter(r => !r.success).length; 94 | 95 | res.status(200).json({ 96 | success: true, 97 | message: 'Broadcast SMS sent', 98 | summary: { 99 | total: recipients.length, 100 | successful, 101 | failed, 102 | details: result 103 | } 104 | }); 105 | } catch (error) { 106 | console.error('Error sending broadcast SMS:', error); 107 | res.status(500).json({ 108 | success: false, 109 | message: error.message 110 | }); 111 | } 112 | }); 113 | 114 | // GET /api/notifications/status - Get notification status 115 | router.get('/status', async (req, res) => { 116 | try { 117 | const smsEnabled = process.env.SMS_ENABLED === 'true'; 118 | const awsRegion = process.env.AWS_REGION || 'YOUR_AWS_REGION'; 119 | const senderId = process.env.SMS_SENDER_ID || 'YOUR_SMS_SENDER_ID'; 120 | 121 | res.status(200).json({ 122 | success: true, 123 | smsConfigured: smsEnabled, 124 | provider: 'AWS SNS', 125 | awsRegion: awsRegion, 126 | senderId: senderId, 127 | instituteName: process.env.INSTITUTE_NAME || 'Not Set', 128 | supportPhone: process.env.SUPPORT_PHONE ? 'Configured' : 'Not Set' 129 | }); 130 | } catch (error) { 131 | res.status(500).json({ 132 | success: false, 133 | message: error.message 134 | }); 135 | } 136 | }); 137 | 138 | // GET /api/notifications/logs - Get all notification logs 139 | router.get('/logs', async (req, res) => { 140 | try { 141 | const AWS = require('aws-sdk'); 142 | const docClient = new AWS.DynamoDB.DocumentClient(); 143 | 144 | const params = { 145 | TableName: process.env.NOTIFICATION_LOGS_TABLE || 'NotificationLogs' 146 | }; 147 | 148 | const result = await docClient.scan(params).promise(); 149 | 150 | res.status(200).json({ 151 | success: true, 152 | count: result.Items.length, 153 | logs: result.Items 154 | }); 155 | } catch (error) { 156 | console.error('Error fetching notification logs:', error); 157 | res.status(500).json({ 158 | success: false, 159 | message: 'Error fetching notification logs', 160 | error: error.message 161 | }); 162 | } 163 | }); 164 | 165 | // Function to check for due fees and send notifications 166 | const checkDueFees = async () => { 167 | try { 168 | const students = await Student.getStudentsWithDueFees(); 169 | 170 | const today = new Date(); 171 | const tomorrow = new Date(today); 172 | tomorrow.setDate(today.getDate() + 1); 173 | 174 | // Filter students with fees due tomorrow 175 | const dueStudents = students.filter(student => { 176 | if (!student.fees || !student.fees.due_date) return false; 177 | const dueDate = new Date(student.fees.due_date); 178 | return dueDate.toDateString() === tomorrow.toDateString() && 179 | student.fees.status !== 'paid'; 180 | }); 181 | 182 | console.log(`Found ${dueStudents.length} students with fees due tomorrow`); 183 | 184 | for (const student of dueStudents) { 185 | const dueDate = new Date(student.fees.due_date).toLocaleDateString('en-IN'); 186 | 187 | // Notify student using utility function 188 | await notifyStudent( 189 | student.phone_no, 190 | student.name, 191 | student.fees.monthly_amount, 192 | dueDate 193 | ); 194 | 195 | console.log(`Notification sent to ${student.name}`); 196 | } 197 | 198 | // Log summary to CloudWatch instead of sending SMS to save costs 199 | if (dueStudents.length > 0) { 200 | console.log(`Summary: ${dueStudents.length} students have pending fees`); 201 | // No SMS notification to owner - we'll rely on database logs instead 202 | // This saves costs and prevents unnecessary "X students" messages 203 | } 204 | } catch (error) { 205 | console.error('Error in checkDueFees:', error); 206 | } 207 | }; 208 | 209 | // Uncomment to enable due fees check every day at 10 AM 210 | /* 211 | setInterval(() => { 212 | const now = new Date(); 213 | if (now.getHours() === 10 && now.getMinutes() === 0) { 214 | checkDueFees(); 215 | } 216 | }, 60 * 1000); 217 | */ 218 | 219 | module.exports = router; -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/pages/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Card, Row, Col, Alert, Spinner, Table, Button } from 'react-bootstrap'; 3 | import { StatsService, FeeService } from '../services/api'; 4 | import { toast } from 'react-toastify'; 5 | 6 | const Dashboard = () => { 7 | const [stats, setStats] = useState({ 8 | totalStudents: 0, 9 | pendingFees: 0, 10 | totalRevenue: 0, 11 | monthlyRevenue: 0, 12 | quarterlyRevenue: 0, 13 | yearlyRevenue: 0, 14 | recentReminders: 0, 15 | dueStudentsTomorrow: [] 16 | }); 17 | const [loading, setLoading] = useState(true); 18 | const [error, setError] = useState(null); 19 | 20 | useEffect(() => { 21 | const fetchDashboardData = async () => { 22 | try { 23 | setLoading(true); 24 | 25 | // Fetch dashboard statistics from our service 26 | const data = await StatsService.getDashboardStats(); 27 | setStats(data); 28 | } catch (err) { 29 | console.error('Error fetching dashboard data:', err); 30 | setError('Failed to load dashboard data. Please try again later.'); 31 | 32 | // Fallback to empty data 33 | setStats({ 34 | totalStudents: 0, 35 | pendingFees: 0, 36 | totalRevenue: 0, 37 | monthlyRevenue: 0, 38 | quarterlyRevenue: 0, 39 | yearlyRevenue: 0, 40 | recentReminders: 0, 41 | dueStudentsTomorrow: [] 42 | }); 43 | } finally { 44 | setLoading(false); 45 | } 46 | }; 47 | 48 | fetchDashboardData(); 49 | }, []); 50 | 51 | const handleMarkAsPaid = async (student) => { 52 | try { 53 | await FeeService.updateStatus(student.id, 'paid'); 54 | toast.success(`Marked ${student.name}'s fee as paid`); 55 | 56 | // Remove this student from the list 57 | setStats(prevStats => ({ 58 | ...prevStats, 59 | dueStudentsTomorrow: prevStats.dueStudentsTomorrow.filter(s => s.id !== student.id) 60 | })); 61 | } catch (error) { 62 | toast.error(`Failed to update fee status: ${error.message}`); 63 | } 64 | }; 65 | 66 | const handleSendReminder = async (student) => { 67 | try { 68 | await FeeService.sendReminder(student.id); 69 | toast.success(`Reminder sent to ${student.name}`); 70 | } catch (error) { 71 | toast.error(`Failed to send reminder: ${error.message}`); 72 | } 73 | }; 74 | 75 | return ( 76 |
77 |

Dashboard

78 | 79 | {error && {error}} 80 | 81 | {loading ? ( 82 |
83 | 84 |

Loading dashboard data...

85 |
86 | ) : ( 87 | <> 88 | {/* First row of stats */} 89 | 90 | 91 | 92 | 93 | Total Students 94 | {stats.totalStudents} 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Pending Fees 103 | {stats.pendingFees} 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | Total Revenue 112 | ₹{stats.totalRevenue?.toLocaleString('en-IN') || '0'} 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Recent Reminders 121 | {stats.recentReminders} 122 | 123 | 124 | 125 | 126 | 127 | {/* Period-based Revenue row */} 128 |

Period Revenue Analysis

129 | 130 | 131 | 132 | 133 | This Month 134 | ₹{stats.monthlyRevenue?.toLocaleString('en-IN') || '0'} 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | This Quarter 143 | ₹{stats.quarterlyRevenue?.toLocaleString('en-IN') || '0'} 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | This Year 152 | ₹{stats.yearlyRevenue?.toLocaleString('en-IN') || '0'} 153 | 154 | 155 | 156 | 157 | 158 | {/* Students with fees due */} 159 | 160 | 161 | 162 | 163 |
164 | Students with Fees Due Tomorrow 165 | {stats.dueStudentsTomorrow?.length || 0} 166 |
167 |
168 | 169 | {stats.dueStudentsTomorrow?.length > 0 ? ( 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | {stats.dueStudentsTomorrow.map(student => ( 182 | 183 | 184 | 185 | 186 | 187 | 206 | 207 | ))} 208 | 209 |
NamePhoneCourseMonthly FeeActions
{student.name}{student.phone_no}{student.course_name}₹{student.fees?.monthly_amount?.toLocaleString('en-IN') || '0'} 188 | 195 | {' '} 196 | 205 |
210 | ) : ( 211 |

212 | No students have fees due tomorrow. All caught up! 👍 213 |

214 | )} 215 |
216 |
217 | 218 |
219 | 220 | )} 221 |
222 | ); 223 | }; 224 | 225 | export default Dashboard; -------------------------------------------------------------------------------- /frontend/aiict.in/admin/src/pages/StudentList.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Table, Button, Form, InputGroup, Card, Alert, Spinner, Dropdown } from 'react-bootstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import { toast } from 'react-toastify'; 5 | import { StudentService, FeeService, ExportService } from '../services/api'; 6 | 7 | const StudentList = () => { 8 | const [students, setStudents] = useState([]); 9 | const [loading, setLoading] = useState(true); 10 | const [error, setError] = useState(null); 11 | const [searchTerm, setSearchTerm] = useState(''); 12 | const [editingStudent, setEditingStudent] = useState(null); 13 | 14 | useEffect(() => { 15 | fetchStudents(); 16 | }, []); 17 | 18 | const fetchStudents = async () => { 19 | try { 20 | setLoading(true); 21 | setError(null); 22 | 23 | // Fetch students using our service 24 | const data = await StudentService.getAll(); 25 | setStudents(data.students || []); 26 | } catch (error) { 27 | console.error('Error fetching students:', error); 28 | setError('Failed to load students. Please try again.'); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | const sendReminder = async (studentId) => { 35 | try { 36 | const student = students.find(s => s.id === studentId); 37 | if (!student) return; 38 | 39 | toast.info(`Sending reminder to ${student.name}...`); 40 | 41 | // Use our service to send the reminder 42 | await FeeService.sendReminder(studentId); 43 | 44 | toast.success(`Reminder sent successfully to ${student.name}`); 45 | } catch (error) { 46 | console.error('Error sending reminder:', error); 47 | toast.error(`Failed to send reminder: ${error.message}`); 48 | } 49 | }; 50 | 51 | const updateFeeStatus = async (studentId, status) => { 52 | try { 53 | // Use our service to update fee status 54 | await FeeService.updateStatus(studentId, status); 55 | 56 | // Update local state 57 | setStudents(students.map(student => 58 | student.id === studentId 59 | ? {...student, fees: {...student.fees, status}} 60 | : student 61 | )); 62 | 63 | toast.success('Fee status updated successfully'); 64 | } catch (error) { 65 | console.error('Error updating status:', error); 66 | toast.error(`Failed to update status: ${error.message}`); 67 | } 68 | }; 69 | 70 | const handleExportStudents = (format) => { 71 | try { 72 | switch(format) { 73 | case 'csv': 74 | ExportService.exportStudentsCSV(); 75 | toast.success('Exporting student data as CSV...'); 76 | break; 77 | case 'json': 78 | ExportService.exportStudentsJSON(); 79 | toast.success('Exporting student data as JSON...'); 80 | break; 81 | default: 82 | ExportService.exportStudentsCSV(); 83 | toast.success('Exporting student data as CSV...'); 84 | } 85 | } catch (error) { 86 | console.error('Error exporting students:', error); 87 | toast.error(`Failed to export: ${error.message}`); 88 | } 89 | }; 90 | 91 | const filteredStudents = students.filter(student => 92 | student.name?.toLowerCase().includes(searchTerm.toLowerCase()) || 93 | student.phone_no?.includes(searchTerm) || 94 | student.registration_no?.toLowerCase().includes(searchTerm.toLowerCase()) || 95 | student.course_name?.toLowerCase().includes(searchTerm.toLowerCase()) || 96 | student.email?.toLowerCase().includes(searchTerm.toLowerCase()) 97 | ); 98 | 99 | const formatDate = (dateString) => { 100 | if (!dateString) return 'N/A'; 101 | const date = new Date(dateString); 102 | return date.toLocaleDateString('en-IN'); 103 | }; 104 | 105 | return ( 106 |
107 |

Student Management

108 | 109 | 110 | 111 |
112 |
Students ({students.length})
113 |
114 | 115 | 116 | Export Data 117 | 118 | 119 | handleExportStudents('csv')}> 120 | CSV 121 | 122 | handleExportStudents('json')}> 123 | JSON 124 | 125 | 126 | 127 | 128 | 131 | 132 |
133 |
134 | 135 | 136 | setSearchTerm(e.target.value)} 140 | /> 141 | 142 | 143 |
144 |
145 | 146 | {loading && ( 147 |
148 | 149 |

Loading students...

150 |
151 | )} 152 | 153 | {error && {error}} 154 | 155 | {!loading && !error && ( 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | {filteredStudents.length > 0 ? ( 172 | filteredStudents.map((student) => ( 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 188 | 189 | 216 | 217 | )) 218 | ) : ( 219 | 220 | 223 | 224 | )} 225 | 226 |
NameReg. NoPhoneCourseBatch TimeMonthly FeeStatusDue DateActions
{student.name}{student.registration_no}{student.phone_no}{student.course_name}{student.batch_time}₹{student.fees?.monthly_amount?.toLocaleString('en-IN') || 'N/A'} 181 | 185 | {student.fees?.status?.toUpperCase() || 'N/A'} 186 | 187 | {formatDate(student.fees?.due_date)} 190 |
191 | 192 | Edit 193 | 194 | 195 | 203 | 204 | 214 |
215 |
221 | {searchTerm ? 'No matching students found' : 'No students added yet'} 222 |
227 | )} 228 |
229 | ); 230 | }; 231 | 232 | export default StudentList; -------------------------------------------------------------------------------- /backend/tests/test-endpoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API Endpoint Verification Script 3 | * For production deployment verification 4 | */ 5 | 6 | const axios = require('axios'); 7 | const chalk = require('chalk'); // For colorful console output 8 | 9 | // Configure API base URL 10 | const API_BASE_URL = 'YOUR_API_ENDPOINT'; 11 | 12 | // Test credentials 13 | const TEST_ADMIN = { 14 | email: 'YOUR_ADMIN_EMAIL', 15 | password: 'YOUR_ADMIN_PASSWORD' // Replace with actual test password 16 | }; 17 | 18 | // Initialize storage for auth token 19 | let authToken = ''; 20 | 21 | // Utility for pretty printing 22 | const print = { 23 | title: (text) => console.log(chalk.cyan.bold(`\n=== ${text} ===`)), 24 | success: (text) => console.log(chalk.green(` ${text}`)), 25 | error: (text) => console.log(chalk.red(` ${text}`)), 26 | info: (text) => console.log(chalk.yellow(` ${text}`)), 27 | json: (obj) => console.log(JSON.stringify(obj, null, 2)), 28 | header: () => console.log(chalk.cyan.bold('\n API ENDPOINT VERIFICATION')), 29 | divider: () => console.log(chalk.gray('--------------------------------------------------')), 30 | summary: (results) => { 31 | const total = results.length; 32 | const passed = results.filter(r => r.status === 'passed').length; 33 | const failed = total - passed; 34 | 35 | console.log(chalk.cyan.bold('\n=== TEST SUMMARY ===')); 36 | console.log(chalk.white(`Total Tests: ${total}`)); 37 | console.log(chalk.green(`Passed: ${passed}`)); 38 | console.log(chalk.red(`Failed: ${failed}`)); 39 | console.log(chalk.cyan.bold('\n=== RESULTS ===')); 40 | 41 | results.forEach(result => { 42 | if (result.status === 'passed') { 43 | console.log(chalk.green(` ${result.name}: PASSED`)); 44 | } else { 45 | console.log(chalk.red(` ${result.name}: FAILED - ${result.error}`)); 46 | } 47 | }); 48 | } 49 | }; 50 | 51 | // Test results collector 52 | const results = []; 53 | 54 | // Helper function for API requests 55 | async function apiRequest(endpoint, method = 'GET', data = null, authRequired = false) { 56 | try { 57 | const headers = authRequired ? { Authorization: `Bearer ${authToken}` } : {}; 58 | 59 | const response = await axios({ 60 | method, 61 | url: `${API_BASE_URL}${endpoint}`, 62 | data, 63 | headers, 64 | validateStatus: () => true // Don't throw on error status codes 65 | }); 66 | 67 | return { 68 | success: response.status >= 200 && response.status < 300, 69 | status: response.status, 70 | data: response.data 71 | }; 72 | } catch (error) { 73 | return { 74 | success: false, 75 | error: error.message, 76 | status: error.response?.status || 500 77 | }; 78 | } 79 | } 80 | 81 | // Test suite functions 82 | async function testPublicEndpoints() { 83 | print.title('Testing Public Endpoints'); 84 | 85 | // Test health endpoint 86 | try { 87 | print.info('Testing /health endpoint...'); 88 | const health = await apiRequest('/health'); 89 | 90 | if (health.success) { 91 | print.success('Health endpoint is working'); 92 | print.json(health.data); 93 | results.push({ name: 'Health Endpoint', status: 'passed' }); 94 | } else { 95 | print.error(`Health endpoint failed: ${health.status}`); 96 | results.push({ name: 'Health Endpoint', status: 'failed', error: `Status ${health.status}` }); 97 | } 98 | } catch (error) { 99 | print.error(`Error testing health endpoint: ${error.message}`); 100 | results.push({ name: 'Health Endpoint', status: 'failed', error: error.message }); 101 | } 102 | 103 | // Test SMS endpoint 104 | try { 105 | print.info('Testing /test endpoint (SMS)...'); 106 | const smsTest = await apiRequest('/test'); 107 | 108 | if (smsTest.success) { 109 | print.success('SMS test endpoint is working'); 110 | print.json(smsTest.data); 111 | results.push({ name: 'SMS Test Endpoint', status: 'passed' }); 112 | } else { 113 | print.error(`SMS test endpoint failed: ${smsTest.status}`); 114 | results.push({ name: 'SMS Test Endpoint', status: 'failed', error: `Status ${smsTest.status}` }); 115 | } 116 | } catch (error) { 117 | print.error(`Error testing SMS endpoint: ${error.message}`); 118 | results.push({ name: 'SMS Test Endpoint', status: 'failed', error: error.message }); 119 | } 120 | } 121 | 122 | async function testAuthentication() { 123 | print.title('Testing Authentication'); 124 | 125 | try { 126 | print.info('Testing /api/auth/login endpoint...'); 127 | const auth = await apiRequest('/api/auth/login', 'POST', TEST_ADMIN); 128 | 129 | if (auth.success && auth.data.token) { 130 | print.success('Authentication successful'); 131 | authToken = auth.data.token; 132 | results.push({ name: 'Authentication', status: 'passed' }); 133 | } else { 134 | print.error(`Authentication failed: ${auth.status}`); 135 | print.json(auth.data); 136 | results.push({ name: 'Authentication', status: 'failed', error: `Status ${auth.status}` }); 137 | } 138 | } catch (error) { 139 | print.error(`Error testing authentication: ${error.message}`); 140 | results.push({ name: 'Authentication', status: 'failed', error: error.message }); 141 | } 142 | } 143 | 144 | async function testStudentEndpoints() { 145 | print.title('Testing Student Endpoints'); 146 | 147 | if (!authToken) { 148 | print.error('Authentication required for student endpoints'); 149 | results.push({ name: 'Student Endpoints', status: 'failed', error: 'No authentication token' }); 150 | return; 151 | } 152 | 153 | try { 154 | print.info('Testing /api/students endpoint...'); 155 | const students = await apiRequest('/api/students', 'GET', null, true); 156 | 157 | if (students.success) { 158 | print.success('Student listing endpoint is working'); 159 | print.info(`Found ${students.data.students?.length || 0} students`); 160 | results.push({ name: 'Student Listing', status: 'passed' }); 161 | } else { 162 | print.error(`Student listing failed: ${students.status}`); 163 | results.push({ name: 'Student Listing', status: 'failed', error: `Status ${students.status}` }); 164 | } 165 | } catch (error) { 166 | print.error(`Error testing student endpoints: ${error.message}`); 167 | results.push({ name: 'Student Listing', status: 'failed', error: error.message }); 168 | } 169 | } 170 | 171 | async function testFeeEndpoints() { 172 | print.title('Testing Fee Endpoints'); 173 | 174 | if (!authToken) { 175 | print.error('Authentication required for fee endpoints'); 176 | results.push({ name: 'Fee Endpoints', status: 'failed', error: 'No authentication token' }); 177 | return; 178 | } 179 | 180 | try { 181 | print.info('Testing /api/fees endpoint...'); 182 | const fees = await apiRequest('/api/fees', 'GET', null, true); 183 | 184 | if (fees.success) { 185 | print.success('Fee listing endpoint is working'); 186 | print.info(`Found ${fees.data.fees?.length || 0} fee records`); 187 | results.push({ name: 'Fee Listing', status: 'passed' }); 188 | } else { 189 | print.error(`Fee listing failed: ${fees.status}`); 190 | results.push({ name: 'Fee Listing', status: 'failed', error: `Status ${fees.status}` }); 191 | } 192 | } catch (error) { 193 | print.error(`Error testing fee endpoints: ${error.message}`); 194 | results.push({ name: 'Fee Listing', status: 'failed', error: error.message }); 195 | } 196 | } 197 | 198 | async function testReminderEndpoints() { 199 | print.title('Testing Reminder Endpoints'); 200 | 201 | if (!authToken) { 202 | print.error('Authentication required for reminder endpoints'); 203 | results.push({ name: 'Reminder Endpoints', status: 'failed', error: 'No authentication token' }); 204 | return; 205 | } 206 | 207 | try { 208 | print.info('Testing /api/reminders/status endpoint...'); 209 | const reminderStatus = await apiRequest('/api/reminders/status', 'GET', null, true); 210 | 211 | if (reminderStatus.success) { 212 | print.success('Reminder status endpoint is working'); 213 | print.json(reminderStatus.data); 214 | results.push({ name: 'Reminder Status', status: 'passed' }); 215 | } else { 216 | print.error(`Reminder status failed: ${reminderStatus.status}`); 217 | results.push({ name: 'Reminder Status', status: 'failed', error: `Status ${reminderStatus.status}` }); 218 | } 219 | } catch (error) { 220 | print.error(`Error testing reminder endpoints: ${error.message}`); 221 | results.push({ name: 'Reminder Status', status: 'failed', error: error.message }); 222 | } 223 | } 224 | 225 | // Main test function 226 | async function runTests() { 227 | print.header(); 228 | print.info(`Testing API at: ${API_BASE_URL}`); 229 | print.divider(); 230 | 231 | await testPublicEndpoints(); 232 | await testAuthentication(); 233 | await testStudentEndpoints(); 234 | await testFeeEndpoints(); 235 | await testReminderEndpoints(); 236 | 237 | print.divider(); 238 | print.summary(results); 239 | } 240 | 241 | // Run tests 242 | runTests().catch(error => { 243 | console.error('Test suite error:', error); 244 | process.exit(1); 245 | }); 246 | --------------------------------------------------------------------------------