├── .DS_Store ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── app.js ├── commitlint.config.js ├── contributing └── CONTRIBUTING.md ├── contributors.md ├── controllers ├── authController.js ├── logController.js └── userController.js ├── dev-data ├── logs-simple.json └── users.json ├── dump.rdb ├── middlewares ├── authMiddleware.js └── checkObjectIdMiddlware.js ├── models ├── logModel.js └── userModel.js ├── package-lock.json ├── package.json ├── readme.md ├── redisClient.js ├── routes ├── logRoutes.js └── userRoutes.js ├── server.js └── utils ├── appError.js └── catchAsync.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSource-GH/dumsor-tracker-api/2b54fcbc6ee62ca609380212c1127bf5b8d8f143/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "semi": [2, "always"], 10 | "require-yield": 0, 11 | "quotes": ["error", "single", { "avoidEscape": true }], 12 | "space-before-function-paren": ["warn", "never"], 13 | "no-unused-vars": "warn" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | 4 | .idea/* 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const morgan = require('morgan'); 3 | const logRouter = require('./routes/logRoutes.js'); 4 | const userRouter = require('./routes/userRoutes.js'); 5 | 6 | const app = express(); 7 | 8 | app.use(express.json()); 9 | 10 | if (process.env.NODE_ENV === 'development') { 11 | app.use(morgan('dev')); 12 | } 13 | 14 | app.use((req, res, next) => { 15 | req.requestTime = new Date().toISOString(); 16 | next(); 17 | }); 18 | 19 | app.use('/api/v1/logs', logRouter); 20 | app.use('/api/v1/users', userRouter); 21 | 22 | //root url 23 | app.get('/', (req, res) => { 24 | res.send('Server Up!'); 25 | }); 26 | 27 | module.exports = app; 28 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 2 | // ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 3 | // docs: Documentation only changes 4 | // feat: A new feature 5 | // fix: A bug fix 6 | // perf: A code change that improves performance 7 | // refactor: A code change that neither fixes a bug nor adds a feature 8 | // style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 9 | // test: Adding missing tests or correcting existing tests 10 | 'use strict'; 11 | module.exports = { 12 | extends: ['@commitlint/config-conventional'], 13 | rules: { 14 | 'body-leading-blank': [1, 'always'], 15 | 'body-max-line-length': [2, 'always', 100], 16 | 'footer-leading-blank': [1, 'always'], 17 | 'footer-max-line-length': [2, 'always', 100], 18 | 'header-max-length': [2, 'always', 100], 19 | 'scope-case': [2, 'always', 'lower-case'], 20 | 'subject-case': [ 21 | 2, 22 | 'never', 23 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], 24 | ], 25 | 'subject-empty': [2, 'never'], 26 | 'subject-full-stop': [2, 'never', '.'], 27 | 'type-case': [2, 'always', 'lower-case'], 28 | 'type-empty': [2, 'never'], 29 | 'type-enum': [ 30 | 2, 31 | 'always', 32 | [ 33 | 'build', 34 | 'chore', 35 | 'ci', 36 | 'docs', 37 | 'feat', 38 | 'fix', 39 | 'perf', 40 | 'refactor', 41 | 'revert', 42 | 'style', 43 | 'test', 44 | 'translation', 45 | 'security', 46 | 'changeset', 47 | ], 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /contributing/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for considering contributing to our project! We welcome all contributions. 4 | 5 | ## Reporting Bugs 6 | 7 | If you encounter a bug, please open an issue on GitHub and provide detailed information about the bug, including steps to reproduce it. 8 | 9 | ## Suggesting Enhancements 10 | 11 | If you have an idea for an enhancement or new feature, please open an issue on GitHub and describe your proposal. 12 | 13 | ## Code Style 14 | 15 | Please follow our coding style guidelines when contributing code to the project. 16 | 17 | ## Submitting Pull Requests 18 | 19 | To contribute code changes, please fork the repository and submit a pull request with your changes. 20 | 21 | ## Code Review 22 | 23 | All pull requests will be reviewed by project maintainers. Please be patient and responsive to feedback during the review process. -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | Thank you to all the contributors who have helped improve this project! 3 | 4 | - Kwesi Dadson 5 | - MawuliKing John Emil 6 | - Elvis Opoku Amoako 7 | - KwakuAldo Aldo Adabunu 8 | - topboyasante Nana K. 9 | - ajagyabeng James Asenso 10 | - starboyles Kwame Sarfo 11 | - ebarthur lasagna 12 | - Khofi Adjei Kingsford (khofiadjei@gmail.com) 13 | 14 | 15 | Special thanks to: 16 | - topboyasante Nana K for starting the project 17 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('./../models/userModel'); 2 | const AppError = require('./../utils/appError'); 3 | const { createClient } = require('@supabase/supabase-js') 4 | const dotenv = require('dotenv'); 5 | 6 | dotenv.config() 7 | const supabaseUrl = process.env.SUPABASE_URL; 8 | const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; 9 | const supabase = createClient(supabaseUrl, supabaseAnonKey); 10 | 11 | exports.signup = async (req, res, next) => { 12 | const { email, password } = req.body; 13 | try { 14 | const { data, error } = await supabase.auth.signUp({ 15 | email, 16 | password, 17 | }); 18 | 19 | if (error) { 20 | return res.status(400).json({ error: error.message }); 21 | } 22 | 23 | res.json({ user: data.user, session: data.session }); 24 | } catch (err) { 25 | return next(new AppError('Failed to sign up user', 500)); 26 | } 27 | }; 28 | 29 | exports.login = async (req, res, next) => { 30 | const { email, password } = req.body; 31 | try { 32 | const { user, session, error } = await supabase.auth.signInWithPassword({ 33 | email, 34 | password, 35 | }); 36 | 37 | if (error) { 38 | return next(new AppError(error.message, 401)); 39 | } 40 | 41 | res.status(200).json({ 42 | status: 'success', 43 | data: { 44 | user, 45 | session, 46 | }, 47 | }); 48 | } catch (err) { 49 | console.log(err) 50 | return next(new AppError('Failed to log in user', 500)); 51 | } 52 | }; 53 | 54 | exports.googleLogin = async (req, res, next) => { 55 | const { google } = req.body; 56 | try { 57 | const { user, session, error } = await supabase.auth.signIn({ 58 | google 59 | }); 60 | if (error) { 61 | return next(new AppError(error.message, 401)); 62 | } 63 | res.status(200).json({ 64 | status: 'success', 65 | data: { 66 | session, 67 | user, 68 | }, 69 | }); 70 | } catch (err) { 71 | return next(new AppError('Failed to log in user', 500)); 72 | } 73 | }; 74 | 75 | exports.logout = async (req, res, next) => { 76 | try { 77 | const { error } = await supabase.auth.signOut(); 78 | if (error) { 79 | return next(new AppError(error.message, 500)); 80 | } 81 | res.status(200).json({ 82 | status: 'success', 83 | message: 'User logged out successfully', 84 | }); 85 | } catch (err) { 86 | return next(new AppError('Failed to log out user', 500)); 87 | } 88 | }; 89 | 90 | -------------------------------------------------------------------------------- /controllers/logController.js: -------------------------------------------------------------------------------- 1 | const Log = require('../models/logModel'); 2 | const redisClient = require('../redisClient'); 3 | const catchAsync = require('./../utils/catchAsync'); 4 | 5 | const CACHE_TTL = 3600; 6 | 7 | exports.getAllLogs = catchAsync(async (req, res, next) => { 8 | const redis = redisClient.getClient(); 9 | const page = parseInt(req.query.page, 10) || 1; 10 | const pageSize = parseInt(req.query.pageSize, 10) || 10; 11 | const skip = (page - 1) * pageSize; 12 | try { 13 | const cacheKey = `logs:${page}:${pageSize}:${req.query.location || 'all'}`; 14 | 15 | const cachedData = await redis.get(cacheKey); 16 | if (cachedData) { 17 | return res.status(200).json(JSON.parse(cachedData)); 18 | } 19 | //If no catch, query the db 20 | let query = {}; 21 | if (req.query.location) { 22 | query = { location: { $regex: new RegExp(req.query.location, 'i') } }; 23 | } 24 | 25 | const [logs, totalLogs] = await Promise.all([ 26 | Log.find(query).skip(skip).limit(pageSize).sort({ createdAt: -1 }), 27 | Log.countDocuments(query), 28 | ]); 29 | 30 | const response = { 31 | status: 'Success', 32 | data: { 33 | results: logs.length, 34 | logs, 35 | page, 36 | pageSize, 37 | totalLogs, 38 | }, 39 | }; 40 | 41 | await redis.setEx(cacheKey, CACHE_TTL, JSON.stringify(response)); 42 | res.status(200).json(response); 43 | } catch (err) { 44 | res.status(404).json({ 45 | status: 'fail', 46 | message: err.message, 47 | }); 48 | } 49 | }); 50 | 51 | exports.getLog = catchAsync(async (req, res, next) => { 52 | try { 53 | 54 | const log = await Log.findById(req.params.id); 55 | if (!log) { 56 | return res.status(404).json({ 57 | status: 'fail', 58 | message: 'Log not found', 59 | }); 60 | } 61 | res.status(200).json({ 62 | status: 'success', 63 | data: { 64 | log, 65 | }, 66 | }); 67 | } catch (error) { 68 | res.status(500).json({ 69 | status: 'error', 70 | message: error.message, 71 | }); 72 | } 73 | }); 74 | 75 | exports.createLog = catchAsync(async (req, res, next) => { 76 | try { 77 | const { location, date, timeOff, timeBackOn, userId } = req.body; 78 | const newLog = await Log.create({ 79 | location, 80 | date, 81 | timeOff, 82 | timeBackOn, 83 | userId, 84 | }); 85 | const redis = redisClient.getClient(); 86 | try { 87 | await redis.set(`log:${newLog._id}`, CACHE_TTL, JSON.stringify(newLog)); 88 | await redis.del('logs:all'); 89 | } catch (redisError) { 90 | console.error('Redis caching error:', redisError); 91 | } 92 | res.status(201).json({ 93 | status: 'success', 94 | data: { 95 | log: newLog, 96 | }, 97 | }); 98 | } catch (err) { 99 | res.status(400).json({ 100 | status: 'fail', 101 | message: err.message, 102 | }); 103 | } 104 | }); 105 | 106 | exports.updateLog = catchAsync(async (req, res, next) => { 107 | try { 108 | const log = await Log.findByIdAndUpdate(req.params.id, req.body, { 109 | new: true, 110 | }); 111 | if (!log) { 112 | return res.status(404).json({ 113 | status: 'fail', 114 | message: 'log not found', 115 | }); 116 | } 117 | res.status(200).json({ 118 | status: 'success', 119 | data: { 120 | log, 121 | }, 122 | }); 123 | } catch (err) { 124 | res.status(500).json({ 125 | status: 'fail', 126 | message: err.message, 127 | }); 128 | } 129 | }); 130 | 131 | exports.deleteLog = catchAsync(async (req, res, next) => { 132 | try { 133 | const log = await Log.findByIdAndDelete(req.params.id); 134 | if (!log) { 135 | return res.status(404).json({ 136 | status: 'fail', 137 | message: 'Log not found', 138 | }); 139 | } 140 | res.status(200).json({ 141 | status: 'success', 142 | data: null, 143 | }); 144 | } catch (err) { 145 | res.status(500).json({ 146 | status: 'fail', 147 | message: err.message, 148 | }); 149 | } 150 | }); 151 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | const User = require('./../models/userModel'); 2 | const catchAsync = require('./../utils/catchAsync'); 3 | 4 | //Added pagination logic to the getAllUsers controller 5 | exports.getAllUsers = catchAsync(async (req, res, next) => { 6 | // Set default values for page and limit 7 | const page = parseInt(req.query.page, 10) || 1; 8 | const pageSize = parseInt(req.query.pageSize, 10) || 10; 9 | 10 | // Calculate the skip based on the page and pageSize 11 | const skip = (page - 1) * pageSize; 12 | 13 | try { 14 | // Find all users with pagination 15 | const users = await User.find().skip(skip).limit(pageSize); 16 | 17 | // Calculate the total number of users 18 | const totalUsers = await User.countDocuments(); 19 | 20 | // Send response with pagination information 21 | res.status(200).json({ 22 | status: 'Success', 23 | data: { 24 | results: users.length, 25 | users, 26 | page, 27 | pageSize, 28 | totalUsers, 29 | }, 30 | }); 31 | } catch (err) { 32 | res.status(404).json({ 33 | status: 'fail', 34 | message: err.message, 35 | }); 36 | } 37 | }); 38 | 39 | exports.createUser = (req, res) => { 40 | res.status(500).json({ 41 | status: 'error', 42 | message: 'this route is not defined yet', 43 | }); 44 | }; 45 | exports.getUser = (req, res) => { 46 | res.status(500).json({ 47 | status: 'error', 48 | message: 'this route is not defined yet', 49 | }); 50 | }; 51 | exports.updateUser = (req, res) => { 52 | res.status(500).json({ 53 | status: 'error', 54 | message: 'this route is not defined yet', 55 | }); 56 | }; 57 | exports.deleteUser = (req, res) => { 58 | res.status(500).json({ 59 | status: 'error', 60 | message: 'this route is not defined yet', 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /dev-data/logs-simple.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "location": "Main Office", 4 | "outages": [ 5 | { 6 | "date": "2024-04-23", 7 | "timeOff": "2024-04-23 10:30:00", 8 | "timeBackOn": "2024-04-23 11:00:00" 9 | }, 10 | { 11 | "date": "2024-04-22", 12 | "timeOff": "2024-04-22 15:45:00", 13 | "timeBackOn": "2024-04-22 16:15:00" 14 | } 15 | ] 16 | }, 17 | { 18 | "location": "Data Center", 19 | "outages": [ 20 | { 21 | "date": "2024-04-23", 22 | "timeOff": "2024-04-23 08:15:00", 23 | "timeBackOn": "2024-04-23 08:30:00" 24 | } 25 | ] 26 | }, 27 | { 28 | "location": "Warehouse", 29 | "outages": [ 30 | { 31 | "date": "2024-04-23", 32 | "timeOff": "2024-04-23 09:45:00", 33 | "timeBackOn": "2024-04-23 10:00:00" 34 | }, 35 | { 36 | "date": "2024-04-23", 37 | "timeOff": "2024-04-23 13:20:00", 38 | "timeBackOn": "2024-04-23 13:40:00" 39 | }, 40 | { 41 | "date": "2024-04-23", 42 | "timeOff": "2024-04-23 16:00:00", 43 | "timeBackOn": "2024-04-23 16:15:00" 44 | } 45 | ] 46 | }, 47 | { 48 | "location": "Office Building", 49 | "outages": [] 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /dev-data/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Kwame Gyamfi", 4 | "email": "jennifer@example.com", 5 | "password": "$2a$12$XCXvvlhRBJ8CydKH09v1v.jpg0hB9gVVfMVEoz4MsxqL9zb5PrF42" 6 | }, 7 | { 8 | "name": "Eliana Shirrer", 9 | "email": "eliana@example.com", 10 | "password": "$2a$12$Jb/ILhdDV.ZpnPMu19xfe.NRh5ntE2LzNMNcsty05QWwRbmFFVMKO" 11 | }, 12 | { 13 | "name": "Steve Arye", 14 | "email": "steve@example.com", 15 | "password": "$2a$12$q7v9dm.S4DvqhAeBc4KwduedEDEkDe2GGFGzteW6xnHt120oRpkqm" 16 | }, 17 | { 18 | "name": "Aarav Arye", 19 | "email": "aarav@example.com", 20 | "password": "$2a$12$lKWhzujFvQwG4m/X3mnTneOB3ib9IYETsOqQ8aN5QEWDjX6X2wJJm" 21 | }, 22 | { 23 | "name": "Miyah Myles", 24 | "email": "miyah@example.com", 25 | "password": "$2a$12$.XIvvmznHQSa9UOI639yhe4vzHKCYO1vpTUZc4d45oiT4GOZQe1kS" 26 | }, 27 | { 28 | "name": "Ben Simmons", 29 | "email": "ben@example.com", 30 | "password": "$2a$12$D3fyuS9ETdBBw5lOwceTMuZcDTyVq28ieeGUAanIuLMcSDz6bpfIe" 31 | }, 32 | { 33 | "name": "Max Smith", 34 | "email": "max@example.com", 35 | "password": "$2a$12$l5qamwqcqC2NlgN6o5A5..9Fxzr6X.bjx/8j3a9jYUHWGOL99oXlm" 36 | }, 37 | { 38 | "name": "Isabel Kirkland", 39 | "email": "isabel@example.com", 40 | "password": "$2a$12$IUnwPH0MGFeMuz7g4gtfvOll.9wgLyxG.9C3TKlttfLtCQWEE6GIu" 41 | }, 42 | { 43 | "name": "Alexander Jones", 44 | "email": "alex@example.com", 45 | "password": "$2a$12$NnclhoYFNcSApoQ3ML8kk.b4B3gbpOmZJLfqska07miAnXukOgK6y" 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSource-GH/dumsor-tracker-api/2b54fcbc6ee62ca609380212c1127bf5b8d8f143/dump.rdb -------------------------------------------------------------------------------- /middlewares/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('util'); 2 | const jwt = require('jsonwebtoken'); 3 | const User = require('./../models/userModel'); 4 | const catchAsync = require('./../utils/catchAsync'); 5 | const AppError = require('./../utils/appError.js'); 6 | 7 | exports.protect = catchAsync(async (req, res, next) => { 8 | // Get token and check if it's there 9 | let token; 10 | if ( 11 | req.headers.authorization && 12 | req.headers.authorization.startsWith('Bearer ') 13 | ) { 14 | token = req.headers.authorization.split(' ')[1]; 15 | } 16 | 17 | if (!token) { 18 | return next(new AppError('You are not logged in. Login to access.', 401)); 19 | } 20 | 21 | //Verification token 22 | const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); 23 | console.log(decoded); 24 | 25 | //Check if user exists 26 | const freshUser = await User.findById(decoded.id); 27 | if (!freshUser) { 28 | return next( 29 | new AppError('The user belonging to this token does no longer exist'), 30 | 401 31 | ); 32 | } 33 | 34 | //Check if user changed password after the token was issued 35 | 36 | next(); 37 | }); 38 | -------------------------------------------------------------------------------- /middlewares/checkObjectIdMiddlware.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // Middleware to check if the provided ID is a valid MongoDB object ID 4 | exports.checkID = (req, res, next) => { 5 | const id = req.params.id; 6 | 7 | // Check if the ID is a valid MongoDB object ID 8 | if (!mongoose.Types.ObjectId.isValid(id)) { 9 | return res.status(404).json({ 10 | status: 'fail', 11 | message: 'Invalid object ID', 12 | }); 13 | } 14 | 15 | next(); 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /models/logModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const logSchema = new mongoose.Schema({ 4 | id: { 5 | type: String, 6 | unique: true, 7 | default: () => new mongoose.Types.ObjectId().toString(), 8 | }, 9 | location: { 10 | type: String, 11 | required: true, 12 | }, 13 | userId: { 14 | type: String, 15 | required: true, 16 | }, 17 | timeOff: { 18 | type: String, 19 | required: true, 20 | }, 21 | timeBackOn: { 22 | type: String, 23 | required: true, 24 | }, 25 | createdAt: { 26 | type: Date, 27 | default: Date.now, 28 | }, 29 | }); 30 | 31 | const Log = mongoose.model('Log', logSchema); 32 | 33 | module.exports = Log; 34 | -------------------------------------------------------------------------------- /models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | const { PhoneNumberUtil } = require('libphonenumber-js'); 4 | 5 | const userSchema = new mongoose.Schema({ 6 | // _id: { 7 | // type: mongoose.Schema.Types.ObjectId, 8 | // auto: true, 9 | // required: [true, 'A user must have an id'], 10 | // unique: true, 11 | // }, 12 | name: { 13 | type: String, 14 | required: [true, 'Make your name known'], 15 | trim: true, 16 | }, 17 | email: { 18 | type: String, 19 | required: ['Provide your email address'], 20 | unique: true, 21 | lowercase: true, 22 | validate: [validator.isEmail, 'Provide a valid or unique email'], 23 | }, 24 | password: { 25 | type: String, 26 | required: [true, 'Provide your password'], 27 | minlength: 8, 28 | select: false, 29 | }, 30 | passwordConfirm: { 31 | type: String, 32 | required: [true, 'Confirm your password'], 33 | validate: { 34 | // Only works on CREATE and SAVE 35 | validator: function (el) { 36 | return el === this.password; 37 | }, 38 | message: 'Passwords are not the same', 39 | }, 40 | }, 41 | phoneNumber: { 42 | type: String, 43 | minlength: 10, 44 | validate: { 45 | validator: function (value) { 46 | try { 47 | const phoneUtil = PhoneNumberUtil.getInstance(); 48 | const phoneNumber = phoneUtil.parseAndKeepRawInput(value, 'GH'); 49 | return phoneUtil.isValidNumber(phoneNumber); 50 | } catch (error) { 51 | return false; 52 | } 53 | }, 54 | message: 'Invalid phone number format', 55 | }, 56 | }, 57 | otp: { 58 | type: String, 59 | select: false, 60 | }, 61 | otpExpiresAt: { 62 | type: Date, 63 | select: false, 64 | }, 65 | isEmailVerified: { 66 | type: Boolean, 67 | default: false, 68 | }, 69 | isPhoneVerified: { 70 | type: Boolean, 71 | default: false, 72 | }, 73 | }); 74 | 75 | // Middleware function to hash the user's password before saving 76 | // Runs before the 'save' operation on the user schema 77 | userSchema.pre('save', async function (next) { 78 | const user = this; 79 | 80 | if (user.isModified('password')) { 81 | user.password = await bcrypt.hash(user.password, 8); 82 | } 83 | next(); 84 | }); 85 | 86 | // Method to convert user object to JSON format 87 | // Removes the password field from the user object before sending to client 88 | userSchema.methods.toJSON = function () { 89 | const user = this; 90 | const userObject = user.toObject(); 91 | delete userObject.password; 92 | 93 | return userObject; 94 | }; 95 | 96 | const User = mongoose.model('User', userSchema); 97 | 98 | module.exports = User; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dumsor-tracker-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server.js", 8 | "lint": "eslint .", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "prepare": "husky install" 11 | }, 12 | "author": "Leslie S. Gyamfi", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@sendgrid/mail": "^7.7.0", 16 | "@supabase/supabase-js": "^2.42.7", 17 | "dotenv": "^16.4.5", 18 | "eslint-config-prettier": "^9.1.0", 19 | "express": "^4.19.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "libphonenumber-js": "^1.10.61", 22 | "moment": "2.30.1", 23 | "mongodb": "^6.5.0", 24 | "mongoose": "^8.3.2", 25 | "morgan": "^1.10.0", 26 | "nodemon": "^3.1.0", 27 | "redis": "^4.7.0", 28 | "supabase": "^1.163.2", 29 | "validator": "^13.11.0" 30 | }, 31 | "devDependencies": { 32 | "@commitlint/cli": "^19.2.2", 33 | "@commitlint/config-conventional": "^19.2.2", 34 | "eslint": "^8.57.1", 35 | "eslint-config-airbnb": "^19.0.4", 36 | "eslint-plugin-node": "^11.1.0", 37 | "eslint-plugin-prettier": "^5.1.3", 38 | "husky": "^9.1.6", 39 | "prettier": "^3.2.5" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Dumsor Tracker Backend 2 | 3 | ### Overview 4 | The Dumsor Tracker project aims to monitor and analyze power outage patterns across the country. 5 | 6 | ### Backend Stack 7 | - Node.js with Express 8 | - MongoDB (Primary DB) 9 | - Supabase (BaaS) - for auth. 10 | 11 | ### Features 12 | - Track power outages and durations. 13 | - View historical data of power outages. 14 | - Manage user schedules and notifications. 15 | 16 | # Setting Up 17 | 18 | ### Prerequisites 19 | - Node.js and npm installed on your machine. 20 | - MongoDB instance set up and running. 21 | - Supabase project created. 22 | 23 | 1. Clone the repository: 24 | `git clone https://github.com/OpenSource-GH/dumsor-tracker-api.git` 25 | 26 | 2. Navigate to the project directory: 27 | 28 | `cd dumsor-tracker-api` 29 | 30 | 4. Install Dependencies 31 | 32 | `npm install` 33 | 34 | # Configuration 35 | 1. ### MongoDB Connection: 36 | 37 | Create a .env file in the project root directory. Add the following environment variable, replacing `` with your actual MongoDB connection URL: 38 | `MONGODB_URL=` 39 | 40 | 2. ### Supabase Configuration: 41 | 42 | Fetch your Supabase project's URL and Anon key from the Supabase dashboard. Set the following environment variables in your .env file: 43 | 44 | `SUPABASE_URL=` 45 | 46 | `SUPABASE_ANON_KEY=` 47 | 48 | 3. ### Run the Server 49 | 50 | `npm start` 51 | 52 | 53 | ### Application Workflow 54 | - Authentication: The application will require authentication. Users can view data without creating an account, but will have to create one to post data. 55 | 56 | - Reporting Power Outages: Users can report power outages by visiting the application and providing the following details: 57 | - Location 58 | - Power Status: Whether the user has electricity or not. 59 | 60 | This streamlined approach will allow for easy data submission and help in tracking and analyzing power outage trends effectively. 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ## Contributing 69 | Contributions are welcome! Please read our [Contribution Guidelines](contributing/CONTRIBUTING.md) before contributing to the project. 70 | 71 | -------------------------------------------------------------------------------- /redisClient.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | 3 | class RedisClient { 4 | constructor() { 5 | this.client = redis.createClient({ 6 | url: process.env.REDIS_URL, 7 | socket: { 8 | reconnectStrategy: (retries) => { 9 | return Math.min(1000 * Math.pow(2, retries), 10000); 10 | }, 11 | }, 12 | }); 13 | 14 | this.client.on('error', (err) => { 15 | console.error('Redis Client Error:', err); 16 | }); 17 | 18 | this.client.on('connect', () => { 19 | console.log('Redis Client Connected'); 20 | }); 21 | 22 | this.client.on('reconnecting', () => { 23 | console.log('Redis Client Reconnecting...'); 24 | }); 25 | 26 | this.client.on('ready', () => { 27 | console.log('Redis Client Ready'); 28 | }); 29 | 30 | this.connect().catch(console.error); 31 | } 32 | 33 | async connect() { 34 | try { 35 | await this.client.connect(); 36 | } catch (err) { 37 | console.error('Redis Connection Error:', err); 38 | throw err; 39 | } 40 | } 41 | 42 | getClient() { 43 | if (!this.client.isOpen) { 44 | throw new Error('Redis client is not connected'); 45 | } 46 | return this.client; 47 | } 48 | 49 | async quit() { 50 | try { 51 | await this.client.quit(); 52 | console.log('Redis connection closed'); 53 | } catch (err) { 54 | console.error('Error closing Redis connection:', err); 55 | throw err; 56 | } 57 | } 58 | } 59 | 60 | const redisClient = new RedisClient(); 61 | module.exports = redisClient; 62 | -------------------------------------------------------------------------------- /routes/logRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const filePath = 3 | './dev-data/logs-simple.json'; 4 | const { protect } = require('../middlewares/authMiddleware.js'); 5 | 6 | const { checkID } = require('../middlewares/checkObjectIdMiddlware.js'); 7 | 8 | const logController = require('./../controllers/logController'); 9 | const logRouter = express.Router(); 10 | 11 | logRouter.param('id', checkID); 12 | 13 | logRouter.route('/').get(logController.getAllLogs).post( 14 | //logController.checkBody, 15 | logController.createLog 16 | ); 17 | 18 | logRouter 19 | .route('/:id') 20 | .get(logController.getLog) 21 | .patch(logController.updateLog) 22 | .delete(logController.deleteLog); 23 | 24 | module.exports = logRouter; 25 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authController = require('./../controllers/authController'); 3 | const userController = require('./../controllers/userController'); 4 | // const { 5 | // validateObjectId, 6 | // } = require('../middlewares/validateObjectIdMiddleware.js'); 7 | 8 | const userRouter = express.Router(); 9 | 10 | userRouter.route('/signup').post(authController.signup); 11 | userRouter.route('/login').post(authController.login); 12 | userRouter.route('/google-login').post(authController.googleLogin); 13 | userRouter.route('/logout').post(authController.logout); 14 | 15 | userRouter 16 | .route('/') 17 | .get(userController.getAllUsers) 18 | .post(userController.createUser); 19 | 20 | userRouter 21 | .route('/:id') 22 | .get( userController.getUser) 23 | .patch(userController.updateUser) 24 | .delete( userController.deleteUser); 25 | 26 | module.exports = userRouter; 27 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const dotenv = require('dotenv'); 3 | const app = require('./app'); 4 | 5 | dotenv.config(); 6 | 7 | const DB = process.env.DATABASE.replace( 8 | '', 9 | process.env.DATABASE_PASSWORD, 10 | ); 11 | mongoose 12 | .connect(DB, 13 | ) 14 | .then(() => console.log('DB Connection Successful')) 15 | .catch((err) => console.error('DB Connection Error:', err)); 16 | 17 | const port = 6000; 18 | 19 | app.listen(port, () => { 20 | console.log(`Dumsor-Tracker running on port ${port}.... `); 21 | }); 22 | -------------------------------------------------------------------------------- /utils/appError.js: -------------------------------------------------------------------------------- 1 | class appError extends Error { 2 | constructor(message, statusCode) { 3 | super(message); 4 | 5 | this.statusCode = statusCode; 6 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; 7 | this.isOperational = true; 8 | 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | 13 | module.exports = appError; 14 | -------------------------------------------------------------------------------- /utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = fn => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | }; 5 | }; --------------------------------------------------------------------------------