first commit of refactor
This commit is contained in:
82
backend/controllers/AddressController.js
Normal file
82
backend/controllers/AddressController.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as AddressModel from '../models/AddressModel.js';
|
||||
|
||||
/**
|
||||
* Create a new address
|
||||
*/
|
||||
export async function create(req, res) {
|
||||
const { address, label } = req.body;
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} creating address: ${address}`);
|
||||
|
||||
if (!address) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Address is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validation: ETH address format
|
||||
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid Ethereum address format'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const newAddress = await AddressModel.create(user_id, address, label || null);
|
||||
console.log(`Address created with ID: ${newAddress.id}`);
|
||||
return res.status(201).json(newAddress);
|
||||
} catch (error) {
|
||||
// Handle duplicate address (unique constraint violation)
|
||||
if (error.code === '23505') {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'You are already tracking this address'
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function list(req, res) {
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} listing addresses`);
|
||||
|
||||
const addresses = await AddressModel.listByUser(user_id);
|
||||
|
||||
console.log(`Found ${addresses.length} addresses for user`);
|
||||
|
||||
return res.json(addresses);
|
||||
}
|
||||
|
||||
export async function remove(req, res) {
|
||||
const addressId = parseInt(req.params.addressId);
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} deleting address ID: ${addressId}`);
|
||||
|
||||
if (isNaN(addressId)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid address ID'
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await AddressModel.remove(addressId, user_id);
|
||||
|
||||
if (!deleted) {
|
||||
console.log(`Address ${addressId} not found or not owned by user`);
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Address not found'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Address ${addressId} deleted`);
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
54
backend/controllers/AlertEventController.js
Normal file
54
backend/controllers/AlertEventController.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as AlertEventModel from '../models/AlertEventModel.js';
|
||||
|
||||
// Mock data for when DB is empty (MVP scaffolding)
|
||||
const MOCK_EVENTS = [
|
||||
{
|
||||
id: 1,
|
||||
alert_rule_id: 1,
|
||||
message: 'Incoming transaction detected: 5.5 ETH received',
|
||||
address_label: 'Treasury Wallet',
|
||||
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString() // 2 hours ago
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
alert_rule_id: 2,
|
||||
message: 'Balance dropped below threshold: Current balance 8.2 ETH',
|
||||
address_label: 'Treasury Wallet',
|
||||
timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString() // 5 hours ago
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
alert_rule_id: 3,
|
||||
message: 'Outgoing transaction detected: 2.0 ETH sent',
|
||||
address_label: 'Cold Storage',
|
||||
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString() // 1 day ago
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
export async function list(req, res) {
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} listing alert events (limit: ${limit})`);
|
||||
|
||||
|
||||
if (limit < 1 || limit > 100) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Limit must be between 1 and 100'
|
||||
});
|
||||
}
|
||||
|
||||
const events = await AlertEventModel.listRecentByUser(user_id, limit);
|
||||
|
||||
console.log(`Found ${events.length} alert events for user`);
|
||||
|
||||
// Return mock data if DB is empty (MVP scaffolding)
|
||||
if (events.length === 0) {
|
||||
return res.json(MOCK_EVENTS.slice(0, limit));
|
||||
}
|
||||
|
||||
return res.json(events);
|
||||
}
|
||||
|
||||
185
backend/controllers/AlertRuleController.js
Normal file
185
backend/controllers/AlertRuleController.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as AlertRuleModel from '../models/AlertRuleModel.js';
|
||||
import * as AddressModel from '../models/AddressModel.js';
|
||||
|
||||
const VALID_ALERT_TYPES = ['incoming_tx', 'outgoing_tx', 'large_transfer', 'balance_below'];
|
||||
const THRESHOLD_REQUIRED_TYPES = ['large_transfer', 'balance_below'];
|
||||
|
||||
/**
|
||||
* Create a new alert rule
|
||||
*/
|
||||
export async function create(req, res) {
|
||||
const addressId = parseInt(req.params.addressId);
|
||||
const { type, threshold } = req.body;
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} creating alert for address ID: ${addressId}`);
|
||||
|
||||
if (isNaN(addressId)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid address ID'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (!type) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Alert type is required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validation: type must be valid
|
||||
if (!VALID_ALERT_TYPES.includes(type)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: `Invalid alert type. Must be one of: ${VALID_ALERT_TYPES.join(', ')}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (THRESHOLD_REQUIRED_TYPES.includes(type)) {
|
||||
if (!threshold || threshold === '') {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: `Alert type '${type}' requires a threshold value`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const thresholdNum = parseFloat(threshold);
|
||||
if (isNaN(thresholdNum) || thresholdNum <= 0) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Threshold must be a positive number'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify address exists AND belongs to this user
|
||||
const address = await AddressModel.findById(addressId, user_id);
|
||||
if (!address) {
|
||||
console.log(`Address ${addressId} not found or not owned by user`);
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Address not found'
|
||||
});
|
||||
}
|
||||
|
||||
const newAlert = await AlertRuleModel.create(
|
||||
addressId,
|
||||
type,
|
||||
threshold || null
|
||||
);
|
||||
|
||||
console.log(`Alert rule created with ID: ${newAlert.id}`);
|
||||
return res.status(201).json(newAlert);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: 'Failed to create alert rule'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function listByAddress(req, res) {
|
||||
const addressId = parseInt(req.params.addressId);
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} listing alerts for address ID: ${addressId}`);
|
||||
|
||||
if (isNaN(addressId)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid address ID'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const address = await AddressModel.findById(addressId, user_id);
|
||||
if (!address) {
|
||||
console.log(`Address ${addressId} not found or not owned by user`);
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Address not found'
|
||||
});
|
||||
}
|
||||
|
||||
const alerts = await AlertRuleModel.listByAddress(addressId);
|
||||
console.log(`Found ${alerts.length} alert rules`);
|
||||
return res.json(alerts);
|
||||
}
|
||||
|
||||
|
||||
export async function updateStatus(req, res) {
|
||||
const alertId = parseInt(req.params.alertId);
|
||||
const { enabled } = req.body;
|
||||
const user_id = req.user_id;
|
||||
|
||||
console.log(`User ${user_id} updating alert ID: ${alertId}`);
|
||||
|
||||
// Validation: alertId must be valid
|
||||
if (isNaN(alertId)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid alert ID'
|
||||
});
|
||||
}
|
||||
|
||||
// Validation: enabled must be boolean
|
||||
if (typeof enabled !== 'boolean') {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'enabled must be a boolean value'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify user owns this alert (through address ownership)
|
||||
const alert = await AlertRuleModel.findById(alertId, user_id);
|
||||
if (!alert) {
|
||||
console.log(`Alert ${alertId} not found or not owned by user`);
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Alert rule not found'
|
||||
});
|
||||
}
|
||||
|
||||
const updatedAlert = await AlertRuleModel.updateEnabled(alertId, enabled);
|
||||
|
||||
console.log(`Alert ${alertId} updated: enabled=${enabled}`);
|
||||
return res.json(updatedAlert);
|
||||
}
|
||||
|
||||
|
||||
export async function remove(req, res) {
|
||||
const alertId = parseInt(req.params.alertId);
|
||||
const user_id = req.user_id;
|
||||
|
||||
console.log(`User ${user_id} deleting alert ID: ${alertId}`);
|
||||
|
||||
if (isNaN(alertId)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid alert ID'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const alert = await AlertRuleModel.findById(alertId, user_id);
|
||||
if (!alert) {
|
||||
console.log(`Alert ${alertId} not found or not owned by user`);
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'Alert rule not found'
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await AlertRuleModel.remove(alertId);
|
||||
|
||||
console.log(`Alert ${alertId} deleted`);
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
105
backend/controllers/NotificationConfigController.js
Normal file
105
backend/controllers/NotificationConfigController.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import * as NotificationConfigModel from '../models/NotificationConfigModel.js';
|
||||
|
||||
/**
|
||||
* Get notification config for the authenticated user
|
||||
*/
|
||||
export async function getConfig(req, res) {
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} getting notification config`);
|
||||
|
||||
const config = await NotificationConfigModel.getConfig(user_id);
|
||||
|
||||
if (!config) {
|
||||
// Return empty config if not set
|
||||
return res.json({
|
||||
user_id,
|
||||
discord_webhook_url: null,
|
||||
telegram_chat_id: null,
|
||||
email: null,
|
||||
notification_enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Config found`);
|
||||
return res.json(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification config for the authenticated user
|
||||
*/
|
||||
export async function updateConfig(req, res) {
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
const { discord_webhook_url, telegram_chat_id, email, notification_enabled } = req.body;
|
||||
|
||||
console.log(`User ${user_id} updating notification config`);
|
||||
|
||||
// Validation: at least one field should be provided
|
||||
if (
|
||||
discord_webhook_url === undefined &&
|
||||
telegram_chat_id === undefined &&
|
||||
email === undefined &&
|
||||
notification_enabled === undefined
|
||||
) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'At least one configuration field must be provided'
|
||||
});
|
||||
}
|
||||
|
||||
// Validation: Discord webhook URL format (basic check)
|
||||
if (discord_webhook_url && !discord_webhook_url.startsWith('https://discord.com/api/webhooks/')) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid Discord webhook URL format'
|
||||
});
|
||||
}
|
||||
|
||||
// Validation: Email format (basic check)
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return res.status(400).json({
|
||||
error: 'VALIDATION_ERROR',
|
||||
message: 'Invalid email address format'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedConfig = await NotificationConfigModel.upsertConfig(user_id, {
|
||||
discord_webhook_url,
|
||||
telegram_chat_id,
|
||||
email,
|
||||
notification_enabled
|
||||
});
|
||||
|
||||
console.log(`Notification config updated`);
|
||||
return res.json(updatedConfig);
|
||||
} catch (error) {
|
||||
console.error('Error updating notification config:', error);
|
||||
return res.status(500).json({
|
||||
error: 'INTERNAL_ERROR',
|
||||
message: 'Failed to update notification configuration'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete notification config for the authenticated user
|
||||
*/
|
||||
export async function deleteConfig(req, res) {
|
||||
const user_id = req.user_id; // From authenticate middleware
|
||||
|
||||
console.log(`User ${user_id} deleting notification config`);
|
||||
|
||||
const deleted = await NotificationConfigModel.remove(user_id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
error: 'NOT_FOUND',
|
||||
message: 'No notification configuration found'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Notification config deleted`);
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user