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"
|
"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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
BIN
backend_patch.zip
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user