171 lines
5.9 KiB
Plaintext
171 lines
5.9 KiB
Plaintext
const mysql = require('mysql2/promise');
|
|
|
|
// Create a connection pool
|
|
const pool = mysql.createPool({
|
|
host: process.env.BILLING_DB_HOST,
|
|
user: process.env.BILLING_DB_USER,
|
|
password: process.env.BILLING_DB_PASS,
|
|
database: process.env.BILLING_DB_NAME,
|
|
waitForConnections: true,
|
|
connectionLimit: 10,
|
|
queueLimit: 0
|
|
});
|
|
|
|
async function getCustomerByPhone(phoneNumber) {
|
|
try {
|
|
// Normalize phone number for search
|
|
// User sends e.g. "62812345678" or "0812345678"
|
|
// Database has "no_wa" column which seems to vary (some have 62 prefix, some 08) based on customer.sql sample
|
|
|
|
// We will try to match broad patterns.
|
|
// If phoneNumber is 628..., we also try 08...
|
|
// If phoneNumber is 08..., we also try 628...
|
|
|
|
let searchNumbers = [phoneNumber];
|
|
let p = phoneNumber;
|
|
|
|
// Clean @c.us suffix
|
|
if (p.endsWith('@c.us')) p = p.replace('@c.us', '');
|
|
|
|
if (p.startsWith('62')) {
|
|
searchNumbers.push('0' + p.substring(2));
|
|
} else if (p.startsWith('0')) {
|
|
searchNumbers.push('62' + p.substring(1));
|
|
}
|
|
|
|
// Add the cleaned number itself just in case
|
|
if (!searchNumbers.includes(p)) searchNumbers.push(p);
|
|
|
|
// Construct query
|
|
const query = `
|
|
SELECT c.name, c.user_profile, c.c_status, c.due_date, c.address, c.no_services,
|
|
pi.name as package_name, pi.price as package_price, pi.description as package_description
|
|
FROM customer c
|
|
LEFT JOIN services s ON c.no_services = s.no_services
|
|
LEFT JOIN package_item pi ON s.item_id = pi.p_item_id
|
|
WHERE c.no_wa IN (?)
|
|
LIMIT 1
|
|
`;
|
|
|
|
const [rows] = await pool.query(query, [searchNumbers]);
|
|
|
|
if (rows.length > 0) {
|
|
return rows[0];
|
|
}
|
|
return null;
|
|
|
|
} catch (error) {
|
|
console.error("Database Error:", error);
|
|
return null; // Fail gracefully
|
|
}
|
|
}
|
|
|
|
// Test connection
|
|
async function testConnection() {
|
|
try {
|
|
const connection = await pool.getConnection();
|
|
console.log("Database connected successfully!");
|
|
connection.release();
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Database connection failed:", error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function searchCustomer(query, limit = 5) {
|
|
try {
|
|
const searchTerm = `%${query}%`;
|
|
const sql = `
|
|
SELECT name, user_profile, c_status, due_date, address, no_services, no_wa
|
|
FROM customer
|
|
WHERE name LIKE ? OR no_wa LIKE ? OR c_status LIKE ? OR user_profile LIKE ?
|
|
LIMIT ?
|
|
`;
|
|
const [rows] = await pool.query(sql, [searchTerm, searchTerm, searchTerm, searchTerm, limit]);
|
|
return rows;
|
|
} catch (error) {
|
|
console.error("Database Search Error:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function getSummary() {
|
|
try {
|
|
const sql = `SELECT c_status, COUNT(*) as total FROM customer GROUP BY c_status`;
|
|
const [rows] = await pool.query(sql);
|
|
return rows;
|
|
} catch (error) {
|
|
console.error("DB Summary Error:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Safe Read-Only SQL execution for Admin AI
|
|
async function executeReadOnlyQuery(sql) {
|
|
try {
|
|
// STRICT SECURITY CHECK
|
|
// 1. Must start with SELECT
|
|
// 2. Must not contain prohibited keywords (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, REPLACE, CREATE, GRANT, REVOKE)
|
|
// 3. Simple whitespace normalization for checking
|
|
const normalizedSql = sql.trim().toUpperCase();
|
|
|
|
if (!normalizedSql.startsWith('SELECT')) {
|
|
throw new Error("Security Violation: Only SELECT queries are allowed.");
|
|
}
|
|
|
|
const prohibited = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE', 'REPLACE', 'CREATE', 'GRANT', 'REVOKE', ';'];
|
|
// Note: Semicolon check prevents multiple statement injection like "SELECT *; DROP TABLE..."
|
|
|
|
for (const word of prohibited) {
|
|
// Check for whole words to avoid false positives (e.g. "UPDATE_DATE" column might trigger "UPDATE")
|
|
// A simple includes check might be too aggressive, but safer.
|
|
// Let's use regex for word boundaries or just strict includes for dangerous keywords used as verbs
|
|
// For now, strict check on prohibited keywords existing loosely
|
|
if (normalizedSql.includes(word + ' ') || normalizedSql.includes(' ' + word) || normalizedSql.endsWith(word)) { // Basic heuristic
|
|
// Better regex:
|
|
const regex = new RegExp(`\\b${word}\\b`);
|
|
if (regex.test(normalizedSql)) {
|
|
throw new Error(`Security Violation: Keyword '${word}' is prohibited.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sql.includes(';')) {
|
|
throw new Error("Security Violation: Multiple statements (;) are not allowed.");
|
|
}
|
|
|
|
const [rows] = await pool.query(sql);
|
|
return rows;
|
|
} catch (error) {
|
|
console.error("SQL Exec Error:", error.message);
|
|
return { error: error.message };
|
|
}
|
|
}
|
|
|
|
async function getSearchCount(query) {
|
|
try {
|
|
const searchTerm = `%${query}%`;
|
|
const sql = `
|
|
SELECT COUNT(*) as total
|
|
FROM customer
|
|
WHERE name LIKE ? OR no_wa LIKE ? OR c_status LIKE ? OR user_profile LIKE ? OR address LIKE ?
|
|
`;
|
|
// Added 'address' to search capabilities as well per user request "jumlah pelanggan di alamat tertentu"
|
|
const [rows] = await pool.query(sql, [searchTerm, searchTerm, searchTerm, searchTerm, searchTerm]);
|
|
return rows[0].total;
|
|
} catch (error) {
|
|
console.error("DB Count Error:", error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getCustomerByPhone,
|
|
searchCustomer,
|
|
getSummary,
|
|
getSearchCount,
|
|
testConnection,
|
|
executeReadOnlyQuery
|
|
};
|