first commit of refactor

This commit is contained in:
KS Jannette
2026-02-27 09:58:18 -05:00
commit 0adfd70853
63 changed files with 10558 additions and 0 deletions

View 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();
}

View 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);
}

View 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();
}

View 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();
}