This commit is contained in:
Gökhan ÖZARSLAN 2026-08-16 20:28:17 +03:00
parent 812f8dceec
commit fe09b726e3
9 changed files with 340 additions and 41 deletions

69
backend/dist/routes/deviceRoutes.js vendored Normal file
View File

@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const VpnService_1 = require("../services/VpnService");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
const vpnService = new VpnService_1.VpnService();
// Helper to generate a VPN IP for a new device
async function getNextVpnIp() {
const baseIp = '10.10.0.';
// Find the highest VPN IP currently in use
const lastDevice = await prisma.device.findFirst({
where: { vpnIp: { startsWith: baseIp } },
orderBy: { vpnIp: 'desc' }
});
if (!lastDevice || !lastDevice.vpnIp) {
return `${baseIp}1`; // Start with 10.10.0.1
}
const lastOctet = parseInt(lastDevice.vpnIp.split('.')[3], 10);
if (lastOctet >= 254) {
throw new Error('VPN IP pool exhausted in this subnet');
}
return `${baseIp}${lastOctet + 1}`;
}
// Get all devices
router.get('/', async (req, res) => {
try {
const devices = await prisma.device.findMany({ include: { tenant: true } });
res.json(devices);
}
catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Create device for a tenant
router.post('/', async (req, res) => {
try {
const { tenantId, name, macAddress, opnsenseUrl, apiKey, apiSecret } = req.body;
// Automatically generate the VPN IP for this customer device
const nextVpnIp = await getNextVpnIp();
const newDevice = await prisma.device.create({
data: {
tenantId,
name,
macAddress,
vpnIp: nextVpnIp,
opnsenseUrl,
apiKey,
apiSecret
},
});
// Trigger OpenVPN config generation via SSH to the VPN Server (10.0.1.150)
try {
await vpnService.generateDeviceVpnConfig(macAddress, nextVpnIp);
console.log(`Successfully generated VPN config for ${macAddress} at ${nextVpnIp}`);
}
catch (vpnErr) {
console.error('Failed to generate VPN config:', vpnErr);
// We don't fail the API request entirely, but log the error
}
res.status(201).json(newDevice);
}
catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal server error' });
}
});
exports.default = router;

32
backend/dist/routes/tenantRoutes.js vendored Normal file
View File

@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = require("express");
const client_1 = require("@prisma/client");
const router = (0, express_1.Router)();
const prisma = new client_1.PrismaClient();
// Get all tenants
router.get('/', async (req, res) => {
try {
const tenants = await prisma.tenant.findMany({
include: { devices: true }
});
res.json(tenants);
}
catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Create tenant
router.post('/', async (req, res) => {
try {
const { name } = req.body;
const newTenant = await prisma.tenant.create({
data: { name },
});
res.status(201).json(newTenant);
}
catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
exports.default = router;

26
backend/dist/server.js vendored Normal file
View File

@ -0,0 +1,26 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const cors_1 = __importDefault(require("cors"));
const client_1 = require("@prisma/client");
const dotenv_1 = __importDefault(require("dotenv"));
dotenv_1.default.config();
const app = (0, express_1.default)();
const prisma = new client_1.PrismaClient();
const PORT = process.env.PORT || 5000;
app.use((0, cors_1.default)());
app.use(express_1.default.json());
// Basic health check endpoint
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', message: '5651 SaaS API is running' });
});
const tenantRoutes_1 = __importDefault(require("./routes/tenantRoutes"));
const deviceRoutes_1 = __importDefault(require("./routes/deviceRoutes"));
app.use('/api/tenants', tenantRoutes_1.default);
app.use('/api/devices', deviceRoutes_1.default);
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

100
backend/dist/services/OpnsenseService.js vendored Normal file
View File

@ -0,0 +1,100 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpnsenseService = void 0;
const axios_1 = __importDefault(require("axios"));
const https_1 = __importDefault(require("https"));
class OpnsenseService {
baseUrl;
apiKey;
apiSecret;
client;
constructor(ipAddress, apiKey, apiSecret) {
// Assuming HTTPS access to OPNsense, might need to adjust port if custom
this.baseUrl = `https://${ipAddress}/api`;
this.apiKey = apiKey;
this.apiSecret = apiSecret;
// OPNsense uses basic auth with API Key and Secret
// We ignore SSL errors because OPNsense often has self-signed certs
const httpsAgent = new https_1.default.Agent({ rejectUnauthorized: false });
this.client = axios_1.default.create({
baseURL: this.baseUrl,
auth: {
username: this.apiKey,
password: this.apiSecret,
},
httpsAgent,
});
}
/**
* Health check to test API connectivity
*/
async testConnection() {
try {
// Fetching core firmware info as a simple ping
const response = await this.client.get('/core/firmware/info');
return response.status === 200;
}
catch (error) {
console.error('OPNsense API Connection Error:', error);
return false;
}
}
/**
* Captive Portal: Add or update a voucher/user session
* This allows the hotspot system to authenticate a user automatically via API
*/
async authHotspotUser(zoneId, username, ipAddress) {
try {
// POST /api/captiveportal/session/connect/
const payload = {
zoneid: zoneId,
user: username,
ip: ipAddress,
};
// OPNsense expects URL encoded form data or specific JSON structure depending on the endpoint
// Using generic captiveportal session connect endpoint
const response = await this.client.post(`/captiveportal/session/connect`, payload);
return response.data;
}
catch (error) {
console.error('Error authenticating hotspot user:', error);
throw new Error('Could not authenticate hotspot user on OPNsense');
}
}
/**
* Captive Portal: Disconnect a user
*/
async disconnectHotspotUser(zoneId, sessionId) {
try {
const payload = {
zoneid: zoneId,
sessionid: sessionId
};
const response = await this.client.post(`/captiveportal/session/disconnect`, payload);
return response.data;
}
catch (error) {
console.error('Error disconnecting hotspot user:', error);
throw new Error('Could not disconnect hotspot user on OPNsense');
}
}
/**
* Traffic Shaper: Add a rule (e.g. for speed limits)
*/
async addShaperRule(ruleData) {
try {
const response = await this.client.post('/trafficshaper/rule/addRule', { rule: ruleData });
// Apply the changes (shaper requires an explicit apply call)
await this.client.post('/trafficshaper/service/reconfigure');
return response.data;
}
catch (error) {
console.error('Error adding shaper rule:', error);
throw new Error('Could not add traffic shaper rule on OPNsense');
}
}
}
exports.OpnsenseService = OpnsenseService;

92
backend/dist/services/VpnService.js vendored Normal file
View File

@ -0,0 +1,92 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VpnService = void 0;
const ssh2_1 = require("ssh2");
class VpnService {
host;
password;
constructor() {
this.host = '10.0.1.150'; // VPN Server IP
this.password = '02!Aug2013'; // Root password
}
/**
* Automatically generates an OpenVPN client config on the VPN server
* and allocates the static VPN IP in the OpenVPN ccd directory.
*/
async generateDeviceVpnConfig(macAddress, vpnIp) {
return new Promise((resolve, reject) => {
const conn = new ssh2_1.Client();
// Clean up the MAC address to use as a client name (remove colons)
const clientName = `device_${macAddress.replace(/:/g, '')}`;
// The shell script to run on the VPN Server (10.0.1.150)
const setupScript = `
#!/bin/bash
cd /etc/openvpn/server
# 1. Create client certificate if it doesn't exist
if [ ! -f "easy-rsa/pki/issued/\${clientName}.crt" ]; then
cd easy-rsa
./easyrsa build-client-full "\${clientName}" nopass
cd ..
fi
# 2. Assign static IP in ccd (Client Config Directory)
mkdir -p ccd
echo "ifconfig-push \${vpnIp} 255.255.255.0" > "ccd/\${clientName}"
# 3. Generate client .ovpn file
cat <<EOF > "/root/\${clientName}.ovpn"
client
dev tun
proto tcp
remote 10.0.1.150 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca ca.crt
cert \${clientName}.crt
key \${clientName}.key
remote-cert-tls server
cipher AES-256-CBC
verb 3
EOF
echo "SUCCESS: VPN Config generated for \${clientName} with IP \${vpnIp}"
`;
conn.on('ready', () => {
conn.exec(setupScript, (err, stream) => {
if (err) {
conn.end();
return reject(err);
}
let output = '';
stream.on('close', (code, signal) => {
conn.end();
if (code === 0) {
resolve(true);
}
else {
reject(new Error(`VPN Script failed with code ${code}. Output: ${output}`));
}
}).on('data', (data) => {
output += data.toString();
console.log('STDOUT: ' + data);
}).stderr.on('data', (data) => {
output += data.toString();
console.error('STDERR: ' + data);
});
});
}).on('error', (err) => {
reject(err);
}).connect({
host: this.host,
port: 22,
username: 'root',
password: this.password,
readyTimeout: 10000
});
});
}
}
exports.VpnService = VpnService;

View File

@ -19,6 +19,7 @@
"ssh2": "^1.17.0" "ssh2": "^1.17.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"@types/ssh2": "^1.15.5", "@types/ssh2": "^1.15.5",
@ -528,6 +529,16 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/d3-array": { "node_modules/@types/d3-array": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",

View File

@ -9,7 +9,6 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"type": "commonjs",
"dependencies": { "dependencies": {
"@prisma/adapter-pg": "^7.9.1", "@prisma/adapter-pg": "^7.9.1",
"@prisma/client": "^7.9.1", "@prisma/client": "^7.9.1",
@ -21,6 +20,7 @@
"ssh2": "^1.17.0" "ssh2": "^1.17.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"@types/ssh2": "^1.15.5", "@types/ssh2": "^1.15.5",

View File

@ -1,44 +1,13 @@
{ {
// Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": { "compilerOptions": {
// File Layout "target": "es2022",
// "rootDir": "./src", "module": "commonjs",
// "outDir": "./dist", "rootDir": "./src",
"outDir": "./dist",
// Environment Settings "esModuleInterop": true,
// See also https://aka.ms/tsconfig/module "forceConsistentCasingInFileNames": true,
"module": "nodenext",
"target": "esnext",
"types": [],
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true, "strict": true,
"jsx": "react-jsx", "skipLibCheck": true
"verbatimModuleSyntax": true, },
"isolatedModules": true, "include": ["src/**/*"]
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
} }

BIN
backend_patch.zip Normal file

Binary file not shown.