kurulum5
This commit is contained in:
parent
812f8dceec
commit
fe09b726e3
69
backend/dist/routes/deviceRoutes.js
vendored
Normal file
69
backend/dist/routes/deviceRoutes.js
vendored
Normal 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
32
backend/dist/routes/tenantRoutes.js
vendored
Normal 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
26
backend/dist/server.js
vendored
Normal 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
100
backend/dist/services/OpnsenseService.js
vendored
Normal 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
92
backend/dist/services/VpnService.js
vendored
Normal 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;
|
||||
11
backend/package-lock.json
generated
11
backend/package-lock.json
generated
@ -19,6 +19,7 @@
|
||||
"ssh2": "^1.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
@ -528,6 +529,16 @@
|
||||
"@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": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz",
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.9.1",
|
||||
"@prisma/client": "^7.9.1",
|
||||
@ -21,6 +20,7 @@
|
||||
"ssh2": "^1.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/ssh2": "^1.15.5",
|
||||
|
||||
@ -1,44 +1,13 @@
|
||||
{
|
||||
// Visit https://aka.ms/tsconfig to read more about this file
|
||||
"compilerOptions": {
|
||||
// File Layout
|
||||
// "rootDir": "./src",
|
||||
// "outDir": "./dist",
|
||||
|
||||
// Environment Settings
|
||||
// See also https://aka.ms/tsconfig/module
|
||||
"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
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
}
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
BIN
backend_patch.zip
Normal file
BIN
backend_patch.zip
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user