Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
node_modules

.next
.env
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,27 @@
# NexaSoft
# NexaSoft

# install the following Packages
- `npm `:

* redis
* nodemailer

* For the .env variables
* 1) Nodemailer Connection
* create ` app password ` in the --- google center --- to use to connect to the Nodemailer module, to be able to send the email
* [ browse on how to create google app password, and follow all steps that would be provided ]
e.g.,
APP_PASSWORD= trye d8wj uwie 9ow0
APP_EMAIL= ruletrek@gmail.com


2) Redis Connection
* create a redis account in the `https://console.upstash.com` platform
* create a DB , and copy your redis API endpoint to use in the redis connection script
* e.g.,
* REDIS_URI=rediss://default:ATavAAIncDI4YzU1ZmVyrhdBN2U0MDBhOGM4MDdkMmMzYWM5MDFiNnAyMTM5OTk@flexible-escargot-13999.upstash.io:6379

* Note: you can read the redis documentation inside the platform how to connect, using different methods

- With that, your connection would be easy to go
-
110 changes: 110 additions & 0 deletions backend/NodeMailer/Interface_Mailing/interface_otp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nexasoft OTP Verification</title>
<style type="text/css">
/* Fallback for clients that support @font-face */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

body, table, td, a { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }

.pulse-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body style="margin: 0; padding: 0; width: 100%; background-color: #f3f4f6;">
<table width="100%" border="0" cellspacing="0" cellpadding="0" style="background-color: #f3f4f6; padding: 40px 20px;">
<tr>
<td align="center">
<div style="max-width: 512px; width: 100%; background-color: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);">

<div style="height: 4px; background: linear-gradient(90deg, #4285f4, #34a853, #fbbc04, #ea4335);"></div>

<div style="padding: 32px 32px 24px 32px; text-align: center; border-bottom: 1px solid #f3f4f6;">
<div style="margin-bottom: 16px; font-size: 30px; font-weight: 700; letter-spacing: -0.025em;">
<span style="color: #111827;">Nexa</span><span style="color: #3b82f6;">soft</span>
</div>

<div style="width: 64px; height: 64px; margin: 0 auto 16px auto; background-color: #eff6ff; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<img src="https://cdn-icons-png.flaticon.com/512/7641/7641727.png" width="32" height="32" alt="Shield" style="display: block; margin: 16px auto;" />
</div>

<h1 style="font-size: 20px; font-weight: 600; color: #1f2937; margin: 0 0 8px 0;">Verification Code</h1>
<p style="font-size: 14px; color: #6b7280; margin: 0;">Enter this code to verify your identity</p>
</div>

<div style="padding: 32px;">
<div style="text-align: center; margin-bottom: 24px;">
<table border="0" cellspacing="0" cellpadding="0" align="center" style="margin: 0 auto;">
<tr>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D1}}</div>
</td>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D2}}</div>
</td>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D3}}</div>
</td>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D4}}</div>
</td>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D5}}</div>
</td>
<td style="padding: 0 4px;">
<div style="width: 48px; height: 56px; background-color: #f9fafb; border: 2px solid #e5e7eb; border-radius: 8px; line-height: 56px; text-align: center; font-size: 24px; font-weight: 700; color: #1f2937;">{{D6}}</div>
</td>
</tr>
</table>
</div>

<div style="text-align: center; margin-bottom: 16px;">
<table border="0" cellspacing="0" cellpadding="0" align="center">
<tr>
<td style="padding-right: 8px;"><div style="width: 8px; height: 8px; background-color: #fb923c; border-radius: 50%;" class="pulse-dot"></div></td>
<td><p style="font-size: 14px; font-weight: 500; color: #4b5563; margin: 0;">OTP Expires in <span style="color: #f97316; font-weight: 600;">1 minute</span></p></td>
</tr>
</table>
</div>

<div style="background-color: #fffbeb; border: 1px solid #fef3c7; border-radius: 12px; padding: 16px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td valign="top" style="padding-right: 12px;">
<div style="width: 32px; height: 32px; background-color: #fef3c7; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span style="color: #d97706; font-weight: bold; font-size: 14px; justify-self: center;">!</span>
</div>
</td>
<td>
<p style="font-size: 14px; font-weight: 600; color: #92400e; margin: 0 0 4px 0;">Security Notice</p>
<p style="font-size: 12px; color: #b45309; margin: 0; line-height: 1.5;">Do not share this code with anyone. Nexasoft will never ask for your OTP via phone or email.</p>
</td>
</tr>
</table>
</div>
</div>

<div style="padding: 24px 32px; background-color: #f9fafb; border-top: 1px solid #f3f4f6; text-align: center;">
<p style="font-size: 12px; color: #9ca3af; margin: 0 0 12px 0;">If you didn't request this code, please ignore this email or contact support.</p>
<p style="font-size: 12px; color: #9ca3af; margin: 0;">
© 2026 Nexasoft &nbsp; • &nbsp; Privacy Policy &nbsp; • &nbsp; Terms of Service
</p>
</div>
</div>

<div style="margin-top: 24px; text-align: center;">
<p style="font-size: 12px; color: #9ca3af; margin: 0;">Secured by Nexasoft Authentication</p>
</div>
</td>
</tr>
</table>
</body>
</html>
54 changes: 54 additions & 0 deletions backend/NodeMailer/SendOTPEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const transporter = require('./Transporter_Mail');
require('dotenv').config();
const fs = require('fs');
const path = require('path');

/**
* Sends the OTP using the interface_otp.html as a static template.
* Injects OTP digits while preserving original HTML/CSS structure.
*/
const SendOTPEmail = async (email, otp) => {
try {
// 1. Locate and Read the HTML template
// Using path.resolve ensures the path is absolute regardless of where the script is run
const templatePath = path.resolve(__dirname, './Interface_Mailing/interface_otp.html');

if (!fs.existsSync(templatePath)) {
throw new Error(`Template not found at: ${templatePath}`);
}

let htmlContent = fs.readFileSync(templatePath, 'utf8');

// 2. Prepare the OTP string
const otpStr = otp.toString().padStart(6, '0'); // Ensures it's always 6 digits

// 3. Inject digits into placeholders {{D1}} through {{D6}}
// This loop replaces each placeholder with the specific digit
for (let i = 0; i < 6; i++) {
const placeholder = `{{D${i + 1}}}`;
htmlContent = htmlContent.replace(placeholder, otpStr[i]);
}

// 4. Configure Email Options
const mailOptions = {
from: `"NexaSoft System" <${process.env.APP_EMAIL}>`,
to: email,
subject: `NexaSoft verification code`,
// The 'html' field treats the string as a full static document
html: htmlContent,
// Added headers for better deliverability
priority: 'high',
};

console.log(`[Mail Service] OTP [${otpStr}] for: ${email}`);

const info = await transporter.sendMail(mailOptions);
return info;

} catch (error) {
console.error("Critical Error: Failed to send OTP email:", error.message);
throw error;
}
};

module.exports = { SendOTPEmail };
14 changes: 14 additions & 0 deletions backend/NodeMailer/Transporter_Mail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const nodemailer = require('nodemailer');
require('dotenv').config();// Load environment variables from .env file

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.APP_EMAIL,
pass: process.env.APP_PASSWORD // Use an app password for Gmail if 2FA is enabled
}
});

module.exports = transporter;


2 changes: 1 addition & 1 deletion backend/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ src/
│ │ ├── user.service.js
│ │ ├── user.repository.js
│ │ └── user.schema.js # Validation (zod/joi)
│ │
│ │ |__ user.verifyOTP.js
│ ├── auth/
│ │ ├── auth.routes.js
│ │ ├── auth.controller.js
Expand Down
34 changes: 34 additions & 0 deletions backend/Redis_config/Redis_setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { createClient } = require('redis');
require('dotenv').config(); // Load environment variables from .env file
// Create the client
const redisClient = createClient({
// use the url from the .env file
url: process.env.REDIS_URI,
socket: {
tls: true, // Crucial for Upstash
reconnectStrategy: (retries) => Math.min(retries * 50, 500)
}
});

// Event listeners for monitoring
redisClient.on('error', (err) => console.error('❌ Redis Client Error:', err));
redisClient.on('connect', () => console.log('⏳ Connecting to Redis...'));
redisClient.on('ready', () => console.log('Connection[Status]: Sucess✅ !\n-----------------------------------------------------\n<< [DEBUGGING SECTION [ACTIVE] >>'));


// Function to initiate connection
const connectRedis = async () => {
try {
if (!redisClient.isOpen) {
await redisClient.connect();
}
} catch (err) {
console.error('❌ Could not connect to Redis:', err);
}
};

// Start the connection immediately when this file is required
connectRedis();

// Export the client so other files can use it
module.exports = redisClient;
15 changes: 13 additions & 2 deletions backend/app.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
const express = require('express');
const userRoutes = require('./modules/user/user.routes');
const db = require('./config/db');
const redisClient= require('./Redis_config/Redis_setup');
require('dotenv').config();
const Port = process.env.PORT || 2000;
const Port = process.env.PORT;
const app = express();

app.use(express.json());
app.use('/nexasoft/users', userRoutes);

// Connect to the DB
db.connect().then(() => {
console.log('Connected to the database');
console.log(`Connected to the database to : ${process.env.DB_NAME} `);
}).catch((err) => {
console.error('Database connection error:', err);
});


// Connect to the Redis Client
app.post('/test-redis', async (req, res) => {
// You can now use the client directly here
await redisClient.set('status', 'running');
const value = await redisClient.get('status');
res.send(`Redis status: ${value}`);
});

app.listen(Port, ()=>{
console.log(`Server is running on port ${Port}`);

})
6 changes: 3 additions & 3 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
require('dotenv').config()
const { pool } = require('pg')
const { Pool } = require('pg')

const db = new pool({
const db = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
database: process.env.DB_NAME
});

module.exports = db
Expand Down
30 changes: 30 additions & 0 deletions backend/config/setup_script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Create the setup script to run the sql file to build the table in the DB
const fs= require('fs');
const path = require('path');
// 1. Force dotenv to look one folder up (in the backend root)


require('dotenv').config({ path: path.resolve(__dirname, '../.env') });


// 2. Add a quick sanity check to prove it worked
console.log("🛠️ Attempting to connect on port:", process.env.DB_PORT);
const db = require('./db'); // import the db connection pool


// Read the SQL file and execute the query to create the users table
const initializeDB= async () => {
try{
const sqlFilePath = path.join(__dirname, '../migrations/001_create_users.sql');
const sql = fs.readFileSync(sqlFilePath, 'utf-8');


// Execute the SQL query
await db.query(sql);
console.log("Database initialized successfully");
} catch (error) {
console.error("Error initializing database:", error);
}

}
initializeDB();
2 changes: 1 addition & 1 deletion backend/migrations/001_create_users.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

CREATE TABLE users (
id SERIAL PRIMARY KEY,

-- profile
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
Expand Down
22 changes: 22 additions & 0 deletions backend/modules/user/HASh-FOLD/hashingOTPService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const argon2 = require('argon2');

/**
* Hashes the OTP using Argon2id.
* Settings are tuned for high security (OWASP recommended).
*/
async function hashOTP(otp) {
try {
return await argon2.hash(otp, {
type: argon2.argon2id, // The most secure variant
hashLength: 16, // 32-byte hash output
memoryCost: 2 ** 16, // 64MB memory usage ---[ High memory consumption to slow down brute-force attacks ]
timeCost: 3, // 3 iterations
parallelism: 1 // Number of threads
});
} catch (err) {
console.error('Error hashing OTP:', err);
throw new Error('Internal Security Error');
}
}

module.exports = { hashOTP};
Loading