diff --git a/README.md b/README.md new file mode 100644 index 0000000..053725c --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Birthmark + +Birthmark is a web automation tool designed to automatically send birthday wishes to your loved ones. This tool leverages background jobs to identify active birthdays for the upcoming day and send personalized messages accordingly, ensuring you never miss a chance to celebrate the special moments of those who matter most to you. + +## Installation + +- Clone the repository: `git clone https://github.com/deedee-code/Birthmark.git` +- Navigate to the project directory: `cd Birthmark` +- Install Dependencies: `npm install` +- Configure the environment variables: Create a .env file in the root directory and add the necessary environment variables. Example: + +``` +PORT=your_port_number +SESSION_SECRET=your_session_secret +POSTGRES_USER=your_postgres_username +POSTGRES_PASSWORD=your_postgres_password +POSTGRES_HOST=your_postgres_host +POSTGRES_DATABASE=your_postgres_database +``` + +- Run the server: `npm run dev` + +## Usage + +- User Signup/Signin: Users registers or login with their generated API key. +- Create Celebrant: Users can create celebrants with their birthday details and their preferred mode of communication (Email, SMS, Automated_Call). +- Create Wishes: Users create and save birthday wishes for different celebrants. +- Create Background Job: A background job runs daily to check to check for birthdays happening he next day. +- Send Wishes: The system automatically sends the birthday wishes through the specified communication method. + +## API Documentation + +https://documenter.getpostman.com/view/26786258/2sA3JT1xKi + +### Support + +For any feedback or collaboration, please contact me on [LinkedIn](https://www.linkedin.com/in/doris-oladotun-owoeye-84a38014b/) diff --git a/db_script/database.sql b/db_script/database.sql index 29a23a4..b63ea70 100644 --- a/db_script/database.sql +++ b/db_script/database.sql @@ -11,7 +11,7 @@ CREATE SCHEMA IF NOT EXISTS celebration AUTHORIZATION devdeedee; CREATE TABLE IF NOT EXISTS celebration.user( id SERIAL PRIMARY KEY, phone_number varchar(32) NOT NULL UNIQUE, - password varchar(32) NOT NULL, + password varchar(128) NOT NULL, api_key varchar(255) NOT NULL, is_admin BOOLEAN DEFAULT FALSE, created_at DATE DEFAULT CURRENT_DATE, @@ -26,24 +26,17 @@ CREATE TABLE IF NOT EXISTS celebration.celebrants ( email VARCHAR(255), phone_number VARCHAR(32), birthdate DATE NOT NULL, - channel_id INTEGER NOT NULL, + channel VARCHAR(50) NOT NULL CHECK (channel IN ('SMS', 'AUTOMATED_CALL', 'EMAIL'));, + channel_id INTEGER NOT NULL REFERENCES celebration.channels(id), is_active BOOLEAN DEFAULT FALSE, created_at DATE DEFAULT CURRENT_DATE, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Create a table called channels for storing the channels through which birthday wishes are sent to the celebrants -CREATE TABLE IF NOT EXISTS celebration.channels ( - id SERIAL PRIMARY KEY, - mode_name INTEGER NOT NULL, - is_disabled Boolean DEFAULT FALSE, - descriiption TEXT DEFAULT NULL -); - -- Create a table called birthday_wishes for storing scheduled birthday wishes CREATE TABLE IF NOT EXISTS celebration.birthday_wishes ( id SERIAL PRIMARY KEY, - celebrant_id INTEGER NOT NULL, + celebrant_id INTEGER NOT NULL REFERENCES celebration.celebrants(id), message Varchar(3000) NOT NULL, scheduled_time TIMESTAMP NOT NULL ); @@ -51,7 +44,7 @@ CREATE TABLE IF NOT EXISTS celebration.birthday_wishes ( -- Create a table called birthday_wish_logs for storing logs of sent birthday wishes CREATE TABLE IF NOT EXISTS celebration.birthday_wish_logs ( id SERIAL PRIMARY KEY, - birthday_wishes_id INTEGER NOT NULL, + birthday_wishes_id INTEGER NOT NULL REFERENCES celebration.birthday_wishes(id), time_sent TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status VARCHAR(32) CHECK (status IN ('Successful', 'Pending', 'Failed')) ); @@ -72,7 +65,6 @@ SELECT table_name FROM information_schema.tables WHERE table_schema = 'celebrati -- Add Foreign Key Constraints to Celebrant Table ALTER TABLE celebration.celebrants - ADD FOREIGN KEY (birthdate_id) REFERENCES celebration.birthdates(id), ADD FOREIGN KEY (channel_id) REFERENCES celebration.channels(id); -- Add Foreign Key Constraints to Birthday_wishes Table @@ -110,6 +102,10 @@ ALTER TABLE celebration.celebrants ALTER TABLE celebration.celebrants DROP COLUMN birthdate_old; --- Set the birthdate column as not null +-- Set the birthdate column as not null in celebrants table +ALTER TABLE celebration.celebrants + ALTER COLUMN birthdate SET NOT NULL; + +-- Add channel column to celebrants table ALTER TABLE celebration.celebrants - ALTER COLUMN birthdate SET NOT NULL; \ No newline at end of file + ADD COLUMN channel VARCHAR(50) NOT NULL CHECK (channel IN ('SMS', 'AUTOMATED_CALL', 'EMAIL')); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5fa9e43..adc02f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,23 +9,35 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.0", "inversify": "^6.0.2", + "joi": "^17.13.0", "morgan": "^1.10.0", + "node-cron": "^3.0.3", + "passport": "^0.7.0", + "passport-custom": "^1.1.1", "pg": "^8.11.5", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "uuid": "^9.0.1" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", "@types/morgan": "^1.9.9", "@types/node": "^20.12.5", + "@types/node-cron": "^3.0.11", + "@types/passport": "^1.0.16", "@types/pg": "^8.11.5", "@types/supertest": "^6.0.2", + "@types/uuid": "^9.0.8", "jest": "^29.7.0", "nodemon": "^3.1.0", "supertest": "^6.3.4", @@ -689,6 +701,19 @@ "node": ">=12" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1080,6 +1105,79 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1169,6 +1267,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1227,6 +1334,15 @@ "@types/send": "*" } }, + "node_modules/@types/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1306,6 +1422,21 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true + }, + "node_modules/@types/passport": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", + "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/pg": { "version": "8.11.5", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.5.tgz", @@ -1434,6 +1565,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -1452,8 +1589,7 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "node_modules/accepts": { "version": "1.3.8", @@ -1488,6 +1624,38 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1507,7 +1675,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -1540,6 +1707,23 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1691,8 +1875,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/basic-auth": { "version": "2.0.1", @@ -1710,6 +1893,19 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1749,7 +1945,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1960,6 +2155,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2029,6 +2232,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2053,8 +2264,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2209,6 +2424,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2226,6 +2446,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2300,8 +2528,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -2472,6 +2699,29 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2580,11 +2830,32 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2608,6 +2879,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2669,7 +2959,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2765,6 +3054,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2806,6 +3100,39 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2864,7 +3191,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2931,7 +3257,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -3658,6 +3983,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.13.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.0.tgz", + "integrity": "sha512-9qcrTyoBmFZRNHeVP4edKqIUEgFzq7MHvTNSDuHSqkpOPtiBkgNgcmTSqmiw1kw9tdKaiddvIDv/eCJDxmqWCA==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3753,7 +4090,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3874,7 +4210,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3882,6 +4217,48 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -3927,6 +4304,49 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4026,6 +4446,17 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4071,7 +4502,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4168,6 +4598,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4181,7 +4647,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4206,6 +4671,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pg": { "version": "8.11.5", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", @@ -4463,6 +4933,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4491,6 +4969,19 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4564,6 +5055,20 @@ "node": ">=10" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4592,7 +5097,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4645,6 +5149,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -4707,8 +5216,7 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/simple-update-notifier": { "version": "2.0.0", @@ -4790,6 +5298,14 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -4807,7 +5323,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4821,7 +5336,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4952,6 +5466,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5013,6 +5543,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-jest": { "version": "29.1.2", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", @@ -5145,6 +5680,17 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -5195,6 +5741,11 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5203,6 +5754,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -5250,6 +5813,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5265,6 +5842,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5285,8 +5870,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/write-file-atomic": { "version": "4.0.2", @@ -5321,8 +5905,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "17.7.2", diff --git a/package.json b/package.json index 1e13693..7744266 100644 --- a/package.json +++ b/package.json @@ -13,23 +13,35 @@ "author": "", "license": "ISC", "dependencies": { + "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "express-session": "^1.18.0", "inversify": "^6.0.2", + "joi": "^17.13.0", "morgan": "^1.10.0", + "node-cron": "^3.0.3", + "passport": "^0.7.0", + "passport-custom": "^1.1.1", "pg": "^8.11.5", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "uuid": "^9.0.1" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", "@types/jest": "^29.5.12", "@types/morgan": "^1.9.9", "@types/node": "^20.12.5", + "@types/node-cron": "^3.0.11", + "@types/passport": "^1.0.16", "@types/pg": "^8.11.5", "@types/supertest": "^6.0.2", + "@types/uuid": "^9.0.8", "jest": "^29.7.0", "nodemon": "^3.1.0", "supertest": "^6.3.4", diff --git a/src/__test__/database.test.ts b/src/__test__/database.test.ts index 859db62..88d87d2 100644 --- a/src/__test__/database.test.ts +++ b/src/__test__/database.test.ts @@ -1,4 +1,4 @@ -import { pool } from "../config/database"; +import { pool } from "../configs/database"; describe("Database connection", () => { it("should connect to the database successfully", () => { diff --git a/src/config/types.ts b/src/config/types.ts deleted file mode 100644 index 4c83626..0000000 --- a/src/config/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -// const TYPES = {}; - -// export default TYPES; diff --git a/src/config/app.ts b/src/configs/app.ts similarity index 57% rename from src/config/app.ts rename to src/configs/app.ts index e2c591f..c8ef5fe 100644 --- a/src/config/app.ts +++ b/src/configs/app.ts @@ -1,24 +1,34 @@ -import express from "express"; +import express, { Application, Request, Response } from "express"; import bodyParser from "body-parser"; import dotenv from "dotenv"; import morgan from "morgan"; +import passport from "passport"; import cors from "cors"; +import session from "express-session"; import serverRoute from "../routes/index"; import { pool } from "./database"; +import scheduleBirthdayWishes from "./cronJobTask"; dotenv.config(); +const secret: string | undefined = process.env.SESSION_SECRET; + export default class App { - private server; + private server: Application; constructor() { this.server = express(); this.config(); this.routes(); this.database(); + this.scheduleBirthdayWishes(); } public config() { + if (!secret) { + throw new Error("SESSION_SECRET not found in .env file."); + } + this.server.use(bodyParser.json()); this.server.use( bodyParser.urlencoded({ @@ -27,6 +37,15 @@ export default class App { ); this.server.use(morgan("dev")); this.server.use(cors()); + this.server.use( + session({ + secret: secret, + resave: false, + saveUninitialized: false, + }) + ); + this.server.use(passport.initialize()); + this.server.use(passport.session()); } public routes() { @@ -43,6 +62,11 @@ export default class App { }); } + public scheduleBirthdayWishes() { + scheduleBirthdayWishes.start(); + console.log("Birthday wishes cron job scheduled."); + } + public start(port: number) { this.server.listen(port, () => { console.log(`Server is running on port: ${port}`); diff --git a/src/configs/cronJobTask.ts b/src/configs/cronJobTask.ts new file mode 100644 index 0000000..636a421 --- /dev/null +++ b/src/configs/cronJobTask.ts @@ -0,0 +1,56 @@ +import cron from "node-cron"; +import { pool } from "./database"; +import { BirthdayWish } from "./types"; +import { sendWishToCelebrant } from "../helpers/utils/birthdayWish"; + +const scheduleBirthdayWishes = cron.schedule("0 0 * * *", async () => { + const client = await pool.connect(); + + // Schedule the job to run every day at midnight + try { + await client.query("BEGIN"); + // Query birthday wishes for today + const result = await client.query(` + SELECT bw.id, bw.celebrant_id, bw.message, cw.username + FROM celebration.birthday_wishes bw + JOIN celebration.celebrants cw ON bw.celebrant_id = cw.id + WHERE DATE_TRUNC('day', bw.scheduled_time) = DATE_TRUNC('day', CURRENT_TIMESTAMP) + ORDER BY bw.scheduled_time ASC; + `); + + const birthdayWishes: BirthdayWish[] = result.rows; + + // Group wishes by celebrants + const groupedWishes: { [celebrantId: number]: BirthdayWish[] } = {}; + birthdayWishes.forEach((wish) => { + if (!groupedWishes[wish.celebrant_id]) { + groupedWishes[wish.celebrant_id] = []; + } + groupedWishes[wish.celebrant_id].push(wish); + }); + + // Send wishes for each celebrant + for (const celebrantId of Object.keys(groupedWishes)) { + const wishes = groupedWishes[parseInt(celebrantId)]; + console.log(`Sending birthday wishes for celebrant ${celebrantId}`); + for (const wish of birthdayWishes) { + await sendWishToCelebrant(wish); + } + } + + await client.query( + `INSERT INTO celebration.bg_jobs_status (job_name, status) VALUES ('Send Birthday Wishes', 'Completed');` + ); + + await client.query("COMMIT"); + } catch (error) { + console.error("Error executing cron job:", error); + await client.query("ROLLBACK"); + } finally { + client.release(); + } +}); + +scheduleBirthdayWishes.start(); + +export default scheduleBirthdayWishes; diff --git a/src/config/database.ts b/src/configs/database.ts similarity index 100% rename from src/config/database.ts rename to src/configs/database.ts diff --git a/src/config/inversify.config.ts b/src/configs/inversify.config.ts similarity index 100% rename from src/config/inversify.config.ts rename to src/configs/inversify.config.ts diff --git a/src/configs/types.ts b/src/configs/types.ts new file mode 100644 index 0000000..18e46c1 --- /dev/null +++ b/src/configs/types.ts @@ -0,0 +1,19 @@ +export interface BirthdayWish { + id: number; + celebrant_id: number; + message: string; + username: string; +} + +export interface Wish { + id: number; + celebrant_id: number; + message: string; +} + +export interface CelebrantDetails { + id: number; + username: string; + email: string; + phone_number: string; +} diff --git a/src/controllers/BirthdayWishes/create.ts b/src/controllers/BirthdayWishes/create.ts new file mode 100644 index 0000000..a7b6e74 --- /dev/null +++ b/src/controllers/BirthdayWishes/create.ts @@ -0,0 +1,48 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; +import { postBirthdayWishValidator } from "../../validators/index"; + +const postWishes = async (req: Request, res: Response) => { + const { error } = postBirthdayWishValidator.validate(req.body); + if (error) { + return res.status(400).json({ message: error.details[0].message }); + } + + const { celebrant_id, message, scheduled_time } = req.body; + + try { + const client = await pool.connect(); + + // Check if the celebrant_id exists in the celebrants table + const celebrantExists = await client.query( + "SELECT COUNT(*) FROM celebration.celebrants WHERE id = $1", + [celebrant_id] + ); + + if (celebrantExists.rows[0].count === "0") { + return res.status(404).json({ error: "Celebrant not found" }); + } + + const result = await pool.query( + "INSERT INTO celebration.birthday_wishes (celebrant_id, message, scheduled_time) VALUES ($1, $2, $3) RETURNING *", + [celebrant_id, message, scheduled_time] + ); + + client.release(); + const createWish = result.rows[0]; + + return res.status(201).json({ + success: true, + message: "BirthdayWish created successfully", + data: createWish, + }); + } catch (error) { + console.error("Error executing query", error); + res.status(500).json({ + success: false, + error: "An error occurred while creating the celebrant details!", + }); + } +}; + +export default postWishes; diff --git a/src/controllers/BirthdayWishes/get.ts b/src/controllers/BirthdayWishes/get.ts new file mode 100644 index 0000000..5c4913d --- /dev/null +++ b/src/controllers/BirthdayWishes/get.ts @@ -0,0 +1,32 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; + +const getCelebrantBirthdayWish = async (req: Request, res: Response) => { + const celebrantId = req.params.celebrantId; + + try { + const client = await pool.connect(); + const result = await client.query( + "SELECT * FROM celebration.birthday_wishes WHERE celebrant_id = $1", + [celebrantId] + ); + + client.release(); + + const celebrantWishes = result.rows; + + res.status(200).json({ + success: true, + message: "Birthday wishes retrieved successfully!", + data: celebrantWishes, + }); + } catch (error) { + console.error("Error executing query", error); + res.status(500).json({ + success: false, + error: "An error occurred while creating the celebrant details!", + }); + } +}; + +export default getCelebrantBirthdayWish; diff --git a/src/controllers/apiAuth.controller.ts b/src/controllers/apiAuth.controller.ts new file mode 100644 index 0000000..9022b55 --- /dev/null +++ b/src/controllers/apiAuth.controller.ts @@ -0,0 +1,67 @@ +import passport from "passport"; +import passportCustom from "passport-custom"; +import bcrypt from "bcrypt"; +const CustomStrategy = passportCustom.Strategy; +import { v4 as uuidv4 } from "uuid"; +import { pool } from "../configs/database"; + +async function hashPassword(password: string) { + const saltRounds = 10; + return await bcrypt.hash(password, saltRounds); +} + +async function generateApiKey(phone_number: string) { + const hashedPhoneNumber = await bcrypt.hash(phone_number, 10); + const uuid = uuidv4(); + const combinedString = `${uuid}${hashedPhoneNumber}`; + return await bcrypt.hash(combinedString, 10); +} + +passport.use( + "custom-api-key", + new CustomStrategy(async (req, done) => { + try { + const phone_number = req.body.phone_number; + const password = req.body.password; + + // Check if the user exists + const query = { + text: "SELECT * FROM celebration.user WHERE phone_number = $1", + values: [phone_number], + }; + const result = await pool.query(query); + + if (result.rows.length > 0) { + const data = result.rows[0]; + const match = await bcrypt.compare(password, data.password); + + if (match) { + // Passwords match, return the user info + return done(null, data); + } else { + // Passwords don't match + return done(null, { message: "Incorrect password" }); + } + } else { + // User doesn't exist, create a new user with API key + const hashedPassword = await hashPassword(password); + const api_key = await generateApiKey(phone_number); + + // const api_key = uuidv4(); // Generate a new UUID for API key + + const insertQuery = { + text: "INSERT INTO celebration.user (phone_number, password, api_key, is_admin) VALUES ($1, $2, $3, $4) RETURNING *", + values: [phone_number, hashedPassword, api_key, true], + }; + const newUserResult = await pool.query(insertQuery); + const data = newUserResult.rows[0]; + + return done(null, data); + } + } catch (error) { + return done(error); + } + }) +); + +export default passport; diff --git a/src/controllers/celebrants/create.ts b/src/controllers/celebrants/create.ts new file mode 100644 index 0000000..2755833 --- /dev/null +++ b/src/controllers/celebrants/create.ts @@ -0,0 +1,60 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; +import { celebrantSchema } from "../../validators/index"; + +async function checkUsernameExists(username: string) { + try { + const client = await pool.connect(); + const result = await client.query( + "SELECT COUNT(*) as count FROM celebration.celebrants WHERE username = $1", + [username] + ); + client.release(); + + return result.rows[0].count > 0; + } catch (error) { + console.error("Error checking username existence:", error); + throw error; + } +} + +const createCelebrant = async (req: Request, res: Response) => { + const { error } = celebrantSchema.validate(req.body); + + if (error) { + return res.status(400).json({ message: error.details[0].message }); + } + + const { username, gender, email, phone_number, birthdate, channel } = + req.body; + + try { + const usernameExists = await checkUsernameExists(username); + + if (usernameExists) { + return res.status(400).json({ message: "Username already exists" }); + } + + const client = await pool.connect(); + const result = await client.query( + "INSERT INTO celebration.celebrants (username, gender, email, phone_number, birthdate, channel, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", + [username, gender, email, phone_number, birthdate, channel, true] + ); + + client.release(); + const newCelebrant = result.rows[0]; + + return res.status(200).json({ + success: true, + message: "New Celebrant has been added successfully.", + data: newCelebrant, + }); + } catch (error) { + console.error("Error executing query", error); + res.status(500).json({ + error: "An error occurred while creating the celebrant details!", + }); + } +}; + +export default createCelebrant; diff --git a/src/controllers/celebrants/delete.ts b/src/controllers/celebrants/delete.ts new file mode 100644 index 0000000..b29be4d --- /dev/null +++ b/src/controllers/celebrants/delete.ts @@ -0,0 +1,29 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; + +const deleteCelebrant = async (req: Request, res: Response) => { + const { id } = req.params; + + const result = await pool.query( + `UPDATE celebration.celebrants + SET is_active = false, + username = FLOOR(10000000 + random() * 90000000), + updated_at = NOW() + WHERE id = $1 + RETURNING *`, + [id] + ); + + const updatedCelebrant = result.rows[0]; + + if (!updatedCelebrant) { + return res.status(404).json({ error: "Celebrant not found" }); + } + + return res.status(204).json({ + success: true, + message: "Celebrant deleted successfully", + }); +}; + +export default deleteCelebrant; diff --git a/src/controllers/celebrants/get-all.ts b/src/controllers/celebrants/get-all.ts new file mode 100644 index 0000000..43b8e79 --- /dev/null +++ b/src/controllers/celebrants/get-all.ts @@ -0,0 +1,64 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; + +const allCelebrants = async (req: Request, res: Response) => { + try { + const { q, channel, birthdate, active, gender, sort, order_by } = req.query; + + let queryString = "SELECT * FROM celebration.celebrants WHERE 1=1"; + const queryParams = []; + + if (q) { + queryString += ` AND username ~* $${queryParams.length + 1}`; + queryParams.push(q); + } + + if (channel) { + queryString += ` AND channel = $${queryParams.length + 1}`; + queryParams.push(channel); + } + + if (birthdate) { + const birthdateString = String(birthdate); // Convert birthdate to string + const [month, day] = birthdateString.split("/"); // Split birthdate string + queryString += ` AND EXTRACT(MONTH FROM birthdate) = EXTRACT(MONTH FROM $${ + queryParams.length + 1 + }) AND EXTRACT(DAY FROM birthdate) = EXTRACT(DAY FROM $${ + queryParams.length + 2 + })`; + queryParams.push(month); + queryParams.push(day); + } + + if (active !== undefined) { + queryString += ` AND is_active = $${queryParams.length + 1}`; + queryParams.push(active); + } + + if (gender) { + queryString += ` AND gender = $${queryParams.length + 1}`; + queryParams.push(gender); + } + + if (sort && order_by) { + if (typeof sort === "string" && typeof order_by === "string") { + queryString += ` ORDER BY ${order_by} ${sort.toUpperCase()}`; + } + } + + // Execute the query + const result = await pool.query(queryString, queryParams); + const celebrants = result.rows; + + res.status(200).json({ + success: true, + message: "Successfully fetched all Celebrants", + data: celebrants, + }); + } catch (error) { + console.error("Error fetching celebrants:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +export default allCelebrants; diff --git a/src/controllers/celebrants/get.ts b/src/controllers/celebrants/get.ts new file mode 100644 index 0000000..064cd26 --- /dev/null +++ b/src/controllers/celebrants/get.ts @@ -0,0 +1,52 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; + +const getACelebrant = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { fields } = req.query; + + let queryString = "SELECT "; + const queryParams = [id]; + + // Check if fields parameter is provided and construct SELECT query accordingly + if (fields) { + const selectedFields = + typeof fields === "string" + ? fields + .split(",") + .map((field) => field.trim()) + .filter((field) => field !== "id") + : []; + + if (selectedFields.length === 0) { + queryString += "*"; + } else { + queryString += selectedFields.join(","); + } + } else { + queryString += "*"; + } + + queryString += " FROM celebration.celebrants WHERE id = $1"; + + // Execute the query + const result = await pool.query(queryString, queryParams); + const celebrant = result.rows[0]; + + if (!celebrant) { + return res.status(404).json({ error: "Celebrant not found" }); + } + + res.status(200).json({ + success: true, + message: "Celebrant Id fetched successfully", + data: celebrant, + }); + } catch (error) { + console.error("Error fetching celebrant:", error); + res.status(500).json({ error: "Internal server error" }); + } +}; + +export default getACelebrant; diff --git a/src/controllers/celebrants/update.ts b/src/controllers/celebrants/update.ts new file mode 100644 index 0000000..9121551 --- /dev/null +++ b/src/controllers/celebrants/update.ts @@ -0,0 +1,81 @@ +import { Request, Response } from "express"; +import { pool } from "../../configs/database"; +import { updateCelebrantValidator } from "../../validators/index"; + +const updateCelebrant = async (req: Request, res: Response) => { + const { id } = req.params; + + const { error } = updateCelebrantValidator.validate(req.body); + + if (error) { + return res.status(400).json({ + message: "Validation error", + error, + }); + } + + const { gender, phone_number, email, birthdate, channel } = req.body; + + try { + const client = await pool.connect(); + + const updates = []; + const values = []; + + if (gender !== undefined) { + updates.push("gender = $1"); + values.push(gender); + } + + if (phone_number !== undefined) { + updates.push("phone_number = $2"); + values.push(phone_number); + } + + if (email !== undefined) { + updates.push("email = $3"); + values.push(email); + } + + if (birthdate !== undefined) { + updates.push("birthdate = $4"); + values.push(birthdate); + } + + if (channel !== undefined) { + updates.push("channel = $5"); + values.push(channel); + } + + const updateQuery = `UPDATE celebration.celebrants SET ${updates.join( + "," + )} WHERE id = $${values.length + 1} RETURNING *;`; + values.push(id); + + const result = await client.query(updateQuery, values); + + client.release(); + + const updatedCelebrant = result.rows[0]; + + if (!updatedCelebrant) { + return res.status(404).json({ + message: "Celebrant not found", + }); + } + + return res.status(200).json({ + success: "true", + message: "Celebrant updated successfully", + data: updatedCelebrant, + }); + } catch (error) { + console.error("Error fetching celebrant:", error); + res.status(500).json({ + message: "Internal server error", + error, + }); + } +}; + +export default updateCelebrant; diff --git a/src/controllers/index.ts b/src/controllers/index.ts index e69de29..c61c322 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -0,0 +1,11 @@ +import passport from "./apiAuth.controller"; + +export default passport; + +export { default as createCelebrant } from "./celebrants/create"; +export { default as allCelebrants } from "./celebrants/get-all"; +export { default as getACelebrant } from "./celebrants/get"; +export { default as updateCelebrant } from "./celebrants/update"; +export { default as deleteCelebrant } from "./celebrants/delete"; +export { default as postWishes } from "./BirthdayWishes/create"; +export { default as getCelebrantBirthdayWish } from "./BirthdayWishes/get"; diff --git a/src/controllers/user.ts b/src/controllers/user.ts deleted file mode 100644 index 2a127a5..0000000 --- a/src/controllers/user.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Request, Response } from "express"; -import { QueryResult } from "pg"; -import { pool } from "../config/database"; diff --git a/src/helpers/utils/birthdayWish.ts b/src/helpers/utils/birthdayWish.ts new file mode 100644 index 0000000..a48a346 --- /dev/null +++ b/src/helpers/utils/birthdayWish.ts @@ -0,0 +1,53 @@ +import { pool } from "../../configs/database"; +import { Wish, CelebrantDetails } from "../../configs/types"; + +async function fetchCelebrantDetails( + celebrantId: number +): Promise { + // Query the database to fetch celebrant details based on celebrant_id + const client = await pool.connect(); + const result = await client.query( + ` + SELECT username, email, phone_number + FROM celebration.celebrants + WHERE id = $1 + `, + [celebrantId] + ); + client.release(); + + if (result.rows.length > 0) { + return result.rows[0]; + } else { + throw new Error(`Celebrant with id ${celebrantId} not found`); + } +} + +async function sendMessage( + celebrantDetails: CelebrantDetails, + message: string +) { + // A function to send a message through the internal messaging system + console.log( + `Sending wish to celebrant ${celebrantDetails.username} through birthmark messaging system: ${message}` + ); +} + +async function sendWishToCelebrant(wish: Wish) { + try { + // A function to fetch celebrant details based on celebrant_id + const celebrantDetails = await fetchCelebrantDetails(wish.celebrant_id); + + // A function to send a message through the messaging system + await sendMessage(celebrantDetails, wish.message); + + console.log(`Wish sent to celebrant ${wish.celebrant_id}`); + } catch (error) { + console.error( + `Error sending wish to celebrant ${wish.celebrant_id}:`, + error + ); + } +} + +export { sendWishToCelebrant }; diff --git a/src/middleware/index.ts b/src/middleware/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/middlewares/authenticateAPI.ts b/src/middlewares/authenticateAPI.ts new file mode 100644 index 0000000..97699b9 --- /dev/null +++ b/src/middlewares/authenticateAPI.ts @@ -0,0 +1,36 @@ +import { Request, Response, NextFunction } from "express"; +import { pool } from "../configs/database"; + +const authenticateApiKey = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const apiKey = req.headers["api_key"]; + + if (!apiKey) { + return res + .status(401) + .json({ success: false, message: "API key is required" }); + } + + try { + const client = await pool.connect(); + const result = await client.query( + "SELECT id FROM celebration.user WHERE api_key = $1", + [apiKey] + ); + client.release(); + + if (result.rows.length === 0) { + return res.status(401).json({ message: "Invalid API key" }); + } + + next(); + } catch (error) { + console.error("Error authenticating user:", error); + res.status(500).json({ message: "Internal server error" }); + } +}; + +export default authenticateApiKey; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 0000000..65e5625 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1 @@ +export { default as authenticateApiKey } from "./authenticateAPI"; diff --git a/src/routes/apiAuth.route.ts b/src/routes/apiAuth.route.ts new file mode 100644 index 0000000..dbe417e --- /dev/null +++ b/src/routes/apiAuth.route.ts @@ -0,0 +1,25 @@ +import express, { Request, Response } from "express"; +import passport from "../controllers/index"; + +const router = express.Router(); + +// Define a route to handle authentication +router.post("/auth", async (req: Request, res: Response, next) => { + // Use passport.authenticate middleware with your custom strategy + await passport.authenticate("custom-api-key", (err: string, user: object) => { + if (err) { + return next(err); // Pass error to Express error handler + } + if (!user) { + return res.status(401).json({ message: "Authentication failed" }); + } + // If authentication succeeds, you can handle the user data as needed + return res.status(200).json({ + message: "Successfully Signed-Up/Signed-In with API Key", + success: true, + user: user, + }); + })(req, res, next); // Call the authenticate middleware with req, res, next +}); + +export default router; diff --git a/src/routes/celebrant.route.ts b/src/routes/celebrant.route.ts new file mode 100644 index 0000000..15f4c77 --- /dev/null +++ b/src/routes/celebrant.route.ts @@ -0,0 +1,27 @@ +import express from "express"; +import { authenticateApiKey } from "../middlewares/index"; +import { + allCelebrants, + createCelebrant, + deleteCelebrant, + getACelebrant, + getCelebrantBirthdayWish, + postWishes, + updateCelebrant, +} from "../controllers/index"; + +const router = express.Router(); + +router.post("/", authenticateApiKey, createCelebrant); +router.get("/", authenticateApiKey, allCelebrants); +router.get("/:id", authenticateApiKey, getACelebrant); +router.patch("/:id", authenticateApiKey, updateCelebrant); +router.delete("/:id", authenticateApiKey, deleteCelebrant); +router.post("/birthday-wish", authenticateApiKey, postWishes); +router.get( + "/birthday-wish/:celebrantId", + authenticateApiKey, + getCelebrantBirthdayWish +); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts index 1439d16..a6ca2ea 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,9 +1,13 @@ import express from "express"; import router from "./serverRoute"; +import authRouter from "./apiAuth.route"; +import celebrantRouter from "./celebrant.route"; const routes = express(); -routes.use("/api", router); +routes.use("/api/v1", router); +routes.use("/api/v1", authRouter); +routes.use("/api/v1/celebrants", celebrantRouter); export default routes; diff --git a/src/server.ts b/src/server.ts index 0bd9318..f454307 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import server from "./config/app"; +import server from "./configs/app"; const port = process.env.PORT || 3000; diff --git a/src/validators/birthdayWish.validate.ts b/src/validators/birthdayWish.validate.ts new file mode 100644 index 0000000..e1a4051 --- /dev/null +++ b/src/validators/birthdayWish.validate.ts @@ -0,0 +1,9 @@ +import joi from "joi"; + +const postBirthdayWishValidator = joi.object({ + celebrant_id: joi.number().integer().required(), + message: joi.string().required(), + scheduled_time: joi.date().iso().required(), +}); + +export default postBirthdayWishValidator; diff --git a/src/validators/celebrant.validate.ts b/src/validators/celebrant.validate.ts new file mode 100644 index 0000000..bec3243 --- /dev/null +++ b/src/validators/celebrant.validate.ts @@ -0,0 +1,12 @@ +import joi from "joi"; + +const createCelebrantSchema = joi.object({ + username: joi.string().required(), + gender: joi.string().valid("M", "F", "O").required(), + email: joi.string().email(), + phone_number: joi.string(), + birthdate: joi.date().iso().required(), + channel: joi.string().required(), +}); + +export default createCelebrantSchema; diff --git a/src/validators/index.ts b/src/validators/index.ts new file mode 100644 index 0000000..6c3a3f2 --- /dev/null +++ b/src/validators/index.ts @@ -0,0 +1,3 @@ +export { default as celebrantSchema } from "./celebrant.validate"; +export { default as updateCelebrantValidator } from "./update.validate"; +export { default as postBirthdayWishValidator } from "./birthdayWish.validate"; diff --git a/src/validators/update.validate.ts b/src/validators/update.validate.ts new file mode 100644 index 0000000..be0049e --- /dev/null +++ b/src/validators/update.validate.ts @@ -0,0 +1,11 @@ +import joi from "joi"; + +const updateCelebrantValidator = joi.object({ + gender: joi.string().valid("M", "F", "O"), + email: joi.string().email(), + phone_number: joi.string(), + birthdate: joi.date().iso(), + channel: joi.string(), +}); + +export default updateCelebrantValidator;