Skip to content
Merged
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
36 changes: 24 additions & 12 deletions backend/middleware/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,28 @@ const sanitizeValue = (value) => {
};

/**
* Validate request body against Joi schema
* Format Joi validation errors into a consistent array of { field, message } objects.
*/
const formatErrors = (joiError) =>
joiError.details.map((d) => ({
field: d.context?.key ?? d.path.join("."),
message: d.message.replace(/['"]/g, ""),
}));

/**
* Validate request body against a Joi schema.
* Returns all validation errors at once (abortEarly: false).
*/
export const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true,
});

if (error) {
return res.status(400).json({
error: error.details[0].message,
field: error.details[0].context.key,
errors: formatErrors(error),
});
}

Expand All @@ -43,18 +53,19 @@ export const validate = (schema) => {
};

/**
* Validate query parameters against Joi schema
* Validate query parameters against a Joi schema.
* Returns all validation errors at once (abortEarly: false).
*/
export const validateQuery = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.query, {
abortEarly: false,
stripUnknown: true,
});

if (error) {
return res.status(400).json({
error: error.details[0].message,
field: error.details[0].context.key,
errors: formatErrors(error),
});
}

Expand All @@ -65,18 +76,19 @@ export const validateQuery = (schema) => {
};

/**
* Validate URL parameters
* Validate URL parameters against a Joi schema.
* Returns all validation errors at once (abortEarly: false).
*/
export const validateParams = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.params, {
abortEarly: false,
stripUnknown: true,
});

if (error) {
return res.status(400).json({
error: error.details[0].message,
field: error.details[0].context.key,
errors: formatErrors(error),
});
}

Expand Down
14 changes: 11 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
"seed": "knex seed:run",
"seed:make": "knex seed:make",
"db:reset": "knex migrate:rollback --all && knex migrate:latest && knex seed:run",
"test:db": "node scripts/test-db-setup.js"
"test:db": "node scripts/test-db-setup.js",
"test": "node --experimental-vm-modules $(pnpm bin)/jest --testPathPattern=tests/ --forceExit"
},
"jest": {
"testEnvironment": "node",
"transform": {}
},
"dependencies": {
"@bull-board/api": "^6.13.0",
Expand Down Expand Up @@ -51,6 +56,9 @@
"starknet": "^7.6.4"
},
"devDependencies": {
"nodemon": "^3.0.1"
"@jest/globals": "^29.0.0",
"jest": "^29.0.0",
"nodemon": "^3.0.1",
"supertest": "^7.0.0"
}
}
}
4 changes: 3 additions & 1 deletion backend/routes/balances.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {
getBalanceByTag,
} from "../controllers/balanceController.js";
import { authenticate } from "../middleware/auth.js";
import { validate } from "../middleware/validation.js";
import { balanceCreateSchema } from "../schemas/balance.js";
import { balanceQueryLimiter } from "../config/rateLimiting.js";

const router = express.Router();

// Apply balance query rate limiter: 1000 per hour per API key/user
router.post("/", authenticate, createBalance);
router.post("/", authenticate, validate(balanceCreateSchema), createBalance);
router.get("/all", authenticate, balanceQueryLimiter, getBalances);
router.get("/", authenticate, balanceQueryLimiter, getBalanceByUser);
router.get("/sync", authenticate, updateUserBalance);
Expand Down
6 changes: 4 additions & 2 deletions backend/routes/kycs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
getKycByUser,
} from "../controllers/kycController.js";
import { authenticate } from "../middleware/auth.js";
import { validate } from "../middleware/validation.js";
import { kycCreateSchema, kycUpdateSchema } from "../schemas/kyc.js";

const router = express.Router();

router.post("/", authenticate, createKyc);
router.post("/", authenticate, validate(kycCreateSchema), createKyc);
router.get("/", authenticate, getKycByUser);
router.get("/:id", authenticate, getKycById);
router.put("/:id", authenticate, updateKyc);
router.put("/:id", authenticate, validate(kycUpdateSchema), updateKyc);
router.delete("/:id", authenticate, deleteKyc);

export default router;
3 changes: 2 additions & 1 deletion backend/routes/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { authenticate } from "../middleware/auth.js";
import { validate, validateQuery } from "../middleware/validation.js";
import { transactionSchema, transactionQuerySchema } from "../schemas/transaction.js";
import { processPaymentSchema } from "../schemas/payment.js";
import { paymentLimiter } from "../config/rateLimiting.js";

const router = express.Router();
Expand All @@ -25,7 +26,7 @@ router.put("/:id", authenticate, paymentLimiter, validate(transactionSchema), up
router.delete("/:id", authenticate, paymentLimiter, deleteTransaction);

// Payment operations
router.post("/payment", authenticate, processPayment);
router.post("/payment", authenticate, paymentLimiter, validate(processPaymentSchema), processPayment);
router.get("/payment/limits", getPaymentLimits);
router.get("/tag/:tag/history", getPaymentHistory);

Expand Down
12 changes: 3 additions & 9 deletions backend/routes/users.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import express from "express";
import {
// getUsers,
// getUserById,
// updateUser,
// deleteUser,
profile,
edit_profile,
dashboard_summary,
} from "../controllers/userController.js";
import { authenticate } from "../middleware/auth.js";
import { validate } from "../middleware/validation.js";
import { editProfileSchema } from "../schemas/user.js";

const router = express.Router();

router.get("/profile", authenticate, profile);
router.get("/dashboard-summary", authenticate, dashboard_summary);
router.post("/profile", authenticate, edit_profile);
// router.get("/", authenticate, getUsers);
// router.get("/:id", authenticate, getUserById);
// router.put("/:id", authenticate, updateUser);
// router.delete("/:id", authenticate, deleteUser);
router.post("/profile", authenticate, validate(editProfileSchema), edit_profile);

export default router;
6 changes: 4 additions & 2 deletions backend/routes/wallets.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
send_to_wallet,
} from "../controllers/walletController.js";
import { authenticate } from "../middleware/auth.js";
import { validate } from "../middleware/validation.js";
import { sendToTagSchema, sendToWalletSchema } from "../schemas/wallet.js";

const router = express.Router();

router.get("/", authenticate, getWalletByUserId);
router.post("/send-to-tag", authenticate, send_to_tag);
router.post("/send-to-wallet", authenticate, send_to_wallet);
router.post("/send-to-tag", authenticate, validate(sendToTagSchema), send_to_tag);
router.post("/send-to-wallet", authenticate, validate(sendToWalletSchema), send_to_wallet);
router.get("/:id", authenticate, getWalletById);
router.put("/:id", authenticate, updateWallet);
router.delete("/:id", authenticate, deleteWallet);
Expand Down
61 changes: 54 additions & 7 deletions backend/schemas/auth.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,63 @@
import Joi from "joi";

const passwordRule = Joi.string()
.min(8)
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.required()
.messages({
"string.min": "Password must be at least 8 characters long",
"string.pattern.base":
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character (@$!%*?&)",
"any.required": "Password is required",
"string.empty": "Password cannot be empty",
});

export const authSchemas = {
register: Joi.object({
tag: Joi.string().min(3).max(50).required(),
email: Joi.string().email().required(),
address: Joi.any().allow("", null),
password: Joi.string().min(6).required(),
role: Joi.any().allow("", null),
tag: Joi.string()
.min(3)
.max(50)
.pattern(/^[a-zA-Z0-9_]+$/)
.required()
.messages({
"string.min": "Tag must be at least 3 characters long",
"string.max": "Tag must be at most 50 characters long",
"string.pattern.base": "Tag may only contain letters, numbers, and underscores",
"any.required": "Tag is required",
"string.empty": "Tag cannot be empty",
}),

email: Joi.string()
.email()
.lowercase()
.required()
.messages({
"string.email": "Please provide a valid email address",
"any.required": "Email is required",
"string.empty": "Email cannot be empty",
}),

password: passwordRule,

address: Joi.string().allow("", null).optional(),
role: Joi.string().valid("user", "admin").allow("", null).optional(),
}),

login: Joi.object({
email: Joi.string().required(),
password: Joi.string().required(),
email: Joi.string()
.email()
.required()
.messages({
"string.email": "Please provide a valid email address",
"any.required": "Email is required",
"string.empty": "Email cannot be empty",
}),

password: Joi.string()
.required()
.messages({
"any.required": "Password is required",
"string.empty": "Password cannot be empty",
}),
}),
};
68 changes: 58 additions & 10 deletions backend/schemas/balance.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,61 @@
import Joi from "joi";

export const balanceSchema = Joi.object({
user_id: Joi.any().allow("", null),
token: Joi.any().allow("", null),
symbol: Joi.any().allow("", null),
chain: Joi.any().allow("", null),
amount: Joi.any().allow("", null),
usd_value: Joi.any().allow("", null),
tag: Joi.any().allow("", null),
address: Joi.any().allow("", null),
auto_convert_threshold: Joi.any().allow("", null),
/**
* Schema for creating a new balance entry.
*/
export const balanceCreateSchema = Joi.object({
token: Joi.string()
.required()
.messages({
"any.required": "Token is required",
"string.empty": "Token cannot be empty",
}),

symbol: Joi.string()
.uppercase()
.max(12)
.required()
.messages({
"string.max": "Symbol must be at most 12 characters",
"any.required": "Symbol is required",
"string.empty": "Symbol cannot be empty",
}),

chain: Joi.string()
.required()
.messages({
"any.required": "Chain is required",
"string.empty": "Chain cannot be empty",
}),

amount: Joi.number()
.min(0)
.optional()
.messages({
"number.min": "Amount cannot be negative",
"number.base": "Amount must be a number",
}),

usd_value: Joi.number().min(0).optional().messages({
"number.min": "USD value cannot be negative",
"number.base": "USD value must be a number",
}),

tag: Joi.string().allow("", null).optional(),

address: Joi.string().allow("", null).optional(),

auto_convert_threshold: Joi.number()
.min(0)
.allow(null)
.optional()
.messages({
"number.min": "Auto-convert threshold cannot be negative",
"number.base": "Auto-convert threshold must be a number",
}),
});

/**
* Alias used in existing route (kept for backwards compatibility).
*/
export const balanceSchema = balanceCreateSchema;
Loading