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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ yarn-error.*
# generated native folders
/ios
/android

# ignore env files
.env

# ignore test files
backend-test/
32 changes: 32 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const express = require('express')
const rateLimiter = require('./middlewares/rate-limiter')
const morgan = require('morgan')
const cors = require('cors')
const routes = require('./routes')
const {errorLogging, notFound} = require('./middlewares/errorHandler')

const app = express()
app.use(morgan('dev'))
app.use(cors({
origin:true
}))
app.use(express.json())
app.use(express.urlencoded({extended:true}))


app.use('/api', rateLimiter)
app.use('/api', routes)

app.get('/', (req,res)=>{
res.status(200).json({
message: 'Welcome to Engiconnect',
health: 'ok'
});
})
app.use(notFound)
app.use(errorLogging)




module.exports = app
14 changes: 14 additions & 0 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@


const { createClient } = require("@supabase/supabase-js");
const dotenv = require('dotenv')
dotenv.config();

const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
)



module.exports = supabase;
122 changes: 122 additions & 0 deletions backend/controllers/authControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const {getUserByWallet, createUser} = require("../models/userModel");
const jwt = require("jsonwebtoken");
const ethers = require("ethers");

const loginController = async (req, res, next) => {
try {
const {wallet_address, signature, message} = req.body;

if (!wallet_address || !signature) return res.status(400).json({error: "Wallet and signature required"});

let user;
const {user: fetchedUser, error} = await getUserByWallet(wallet_address);
user = fetchedUser;

if (!user) {
const {data: createdUser, error} = await createUser(wallet_address);
createUser ? (user = createdUser) : null;
error ? res.status(404).json({error: "Error creating user"}) : null;
}

let recovered;
try {
recovered = ethers.verifyMessage(message, signature);
} catch (err) {
return res.status(400).json({error: "Invalid signature format"});
}

if (recovered.toLowerCase() !== wallet_address.toLowerCase()) {
return res.status(401).json({error: "Signature does not match wallet"});
}

const access_token = jwt.sign({wallet_address, sub: user.id, permissions: ["read", "write", "admin"]}, process.env.JWT_SECRET, {
expiresIn: "1h",
});

const refresh_token = jwt.sign({wallet_address, sub: user.id, permissions: ["read", "write", "admin"]}, process.env.JWT_SECRET, {
expiresIn: "7d",
});
{
}

return res.status(200).json({
message: "Login successful",
data: {
access_token,
refresh_token,
user: {id: user.id, wallet_address: user.wallet_address, created_at: user.created_at},
expires_in: 3600,
},
});
} catch (err) {
console.log(err);
next(err);
}
};

const refreshController = async (req, res, next) => {
const {refresh_token} = req.body;

if (!refresh_token) {
return res.status(400).json({error: "refresh_token required"});
}

try {
// Verify refresh token
const decoded = jwt.verify(refresh_token, process.env.JWT_SECRET);

const {user, error} = await getUserByWallet(decoded.wallet_address);

if (error) {
return res.status(400).json({NOT_FOUND: "user does not exist"});
}
// Issue a new access token
const newAccessToken = jwt.sign({wallet: decoded.wallet_address, sub: user.id, permissions: ["read", "write", "admin"]}, process.env.JWT_SECRET, {expiresIn: "1h"});

return res.status(200).json({
access_token: newAccessToken,
expires_in: 3600,
});
} catch (err) {
console.log(err);
return res.status(401).json({error: "Invalid or expired refresh token"});
}
};

const verifyEnsController = async (req, res, next) => {
const {domain, signature, message} = req.body;
if (!message) return res.status(400).json({error: "message is required"});

if (!domain || !signature) {
return res.status(400).json({error: "domain and signature required"});
}
try {
// 1. Resolve ENS domain to address
const provider = new ethers.InfuraProvider("mainnet", process.env.INFURA_API_KEY);
const resolvedAddress = await provider.resolveName(domain);

if (!resolvedAddress) {
return res.status(404).json({error: "ENS domain not found"});
}

// 2. Recover address from signature
const recovered = ethers.verifyMessage(message, signature);

if (recovered.toLowerCase() !== resolvedAddress.toLowerCase()) {
return res.status(401).json({error: "Signature does not match ENS owner"});
}

return res.status(200).json({
message: "ENS verified successfully",
data: {
verified: true,
address: resolvedAddress,
},
});
} catch (err) {
console.error(err);
return res.status(500).json({error: "Verification failed"});
}
};

module.exports = {loginController, refreshController, verifyEnsController};
85 changes: 85 additions & 0 deletions backend/controllers/usersControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const {getUserByWallet, updateUser, searchUsers} = require("../models/userModel");

const getProfileController = async (req, res, next) => {
try {
const {wallet_address} = req.user;
if (!wallet_address) return res.status(400).json({message: "Wallet address missing in request"});

const {user, error} = await getUserByWallet(wallet_address);

if (error) {
return res.status(500).json({message: "Database error", error});
}

user
? res.status(200).json({
message: "User Data",
data: {
...user,
},
})
: res.status(404).json({message: "User not found"});
} catch (err) {
console.log(err);
next(err);
}
};

const searchUsersController = async (req, res, next) => {
const limit = +req.query.limit || 10;
const q = req.query.q;
if (!q) return res.status(400).json({message: "Search term is required"});

try {
const {wallet_address} = req.user;
if (!wallet_address) return res.status(400).json({message: "Wallet address missing in request"});

const {data, error} = await searchUsers(q, limit);

if (error) {
return res.status(500).json({message: "Database error", error});
}
const hasMore = data.length > limit;

// Trim the extra item if it existed
const users = hasMore ? data.slice(0, limit) : data;
data
? res.status(200).json({
message: "User Data",
data: {
data: users,
total: users.length,
has_more: hasMore,
},
})
: res.status(404).json({message: "User not found"});
} catch (err) {
console.log(err);
next(err);
}
};

const updateUserController = async (req, res, next) => {
try {
const {wallet_address} = req.user;
if (!wallet_address) return res.status(400).json({message: "Wallet address missing in request"});

const {data, error} = await updateUser(wallet_address, req.body);

if (error) {
return res.status(500).json({message: "Database error", error: error.message});
}

data
? res.status(201).json({
message: "User Data",
data,
})
: res.status(404).json({message: "User not found"});
} catch (err) {
console.log(err);
next(err);
}
};

module.exports = {getProfileController, updateUserController, searchUsersController};
18 changes: 18 additions & 0 deletions backend/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

const app = require('./app')

const dotenv = require('dotenv')
dotenv.config();
(async()=>{

// GracefulShutdown
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
process.on(signal, async () => {
console.log(`Received ${signal}, shutting down gracefully...`);
})});


app.listen(process.env.PORT || 4000, () => {
console.log(`Server is running on port ${process.env.PORT || 4000}`);
})
})()
26 changes: 26 additions & 0 deletions backend/middlewares/authGuard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const jwt = require("jsonwebtoken");

const authMiddleware = (req, res, next) => {
let token;
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith("Bearer ")) {
token = authHeader.split(" ")[1];
} else if (req.body.token) {
// Fallback: read from body
token = req.body.token;
}
if (!token) {
return res.status(401).json({error: "Token missing"});
}

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
console.log(err);
return res.status(401).json({error: "Invalid or expired token"});
}
};

module.exports = authMiddleware;
12 changes: 12 additions & 0 deletions backend/middlewares/errorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const notFound=(req,res,next)=>{

return res.status(404).json({message:'Invalid url',url:req.originalUrl})
}

const errorLogging =(err,req,res,next)=>{

console.log(err.stack)
return res.status(500).json({message:'Server error'})
}

module.exports = { errorLogging, notFound}
12 changes: 12 additions & 0 deletions backend/middlewares/rate-limiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const rateLimit = require ("express-rate-limit");


const rateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, please try again after 15 minutes'
});

module.exports = rateLimiter
27 changes: 27 additions & 0 deletions backend/middlewares/validate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const validate = (Schema) => async (req, res, next) => {
try {
const result = Schema.safeParse(req.body);
if (!result.success) {
const issue = result.error.issues[0];

let errorMessage;
if (issue.code === "unrecognized_keys") {
errorMessage = `${issue.keys[0]} is not allowed`;
} else {
errorMessage = issue.message;
}
return res.status(422).json({
message: "Validation failed",
error: errorMessage,
});
}

req.validated = result.data;
next();
} catch (error) {
console.log(error);
return res.status(400).json({message: "Invalid request", error});
}
};

module.exports = validate;
Loading