This commit is contained in:
Gökhan ÖZARSLAN 2026-08-16 16:30:08 +03:00
parent ced2c9d61b
commit 7e222bacbf
8 changed files with 679 additions and 159 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,19 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@prisma/adapter-pg": "^7.9.1",
"@prisma/client": "^7.9.1",
"axios": "^1.19.0",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"pg": "^8.23.0"
"pg": "^8.23.0",
"ssh2": "^1.17.0"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^26.2.0",
"@types/ssh2": "^1.15.5",
"nodemon": "^3.1.14",
"prisma": "^7.9.1",
"ts-node": "^10.9.2",

73
backend/simulate.js Normal file
View File

@ -0,0 +1,73 @@
const { PrismaClient } = require('@prisma/client');
const { Pool } = require('pg');
const { PrismaPg } = require('@prisma/adapter-pg');
const { Client } = require('ssh2');
const pool = new Pool({ connectionString: 'postgresql://saas_user:saas_secure_pass_2026@10.0.1.151:5432/saas_5651?schema=public' });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const host = '10.0.1.150';
const password = '02!Aug2013';
async function generateDeviceVpnConfig(macAddress, vpnIp) {
return new Promise((resolve, reject) => {
const conn = new Client();
const clientName = `device_${macAddress.replace(/:/g, '')}`;
const setupScript = `
#!/bin/bash
cd /etc/openvpn/server
# 1. Create client certificate
if [ ! -f "easy-rsa/pki/issued/${clientName}.crt" ]; then
cd easy-rsa
./easyrsa --batch build-client-full "${clientName}" nopass
cd ..
fi
# 2. Assign static IP
mkdir -p ccd
echo "ifconfig-push ${vpnIp} 255.255.255.0" > "ccd/${clientName}"
echo "SUCCESS: VPN Config generated for ${clientName} with IP ${vpnIp}"
`;
conn.on('ready', () => {
conn.exec(setupScript, (err, stream) => {
if (err) return reject(err);
stream.on('close', (code) => {
conn.end();
if (code === 0) resolve(true);
else reject(new Error('Script failed with code ' + code));
}).on('data', data => console.log('STDOUT: ' + data))
.stderr.on('data', data => console.error('STDERR: ' + data));
});
}).connect({ host, port: 22, username: 'root', password, readyTimeout: 10000 });
});
}
async function runTest() {
console.log('--- Starting Test: SaaS E2E Tenant & Device Creation ---');
console.log('1. Creating new Tenant: "Test Hotel A.S."');
const tenant = await prisma.tenant.create({ data: { name: 'Test Hotel A.S.' } });
console.log('2. Allocated VPN IP for new device: 10.10.0.1');
const mac = '00:11:22:33:44:55';
await prisma.device.create({
data: {
tenantId: tenant.id,
name: 'Test Hotel Gateway',
macAddress: mac,
vpnIp: '10.10.0.1'
}
});
console.log('3. Connecting to VPN Server (10.0.1.150) to generate config...');
await generateDeviceVpnConfig(mac, '10.10.0.1');
console.log('--- Test Completed ---');
}
runTest().catch(console.error).finally(() => prisma.$disconnect());

59
backend/simulate.ts Normal file
View File

@ -0,0 +1,59 @@
import { PrismaClient } from '@prisma/client';
import { VpnService } from './src/services/VpnService';
const prisma = new PrismaClient();
const vpnService = new VpnService();
async function runTest() {
console.log('--- Starting Test: SaaS E2E Tenant & Device Creation ---');
// 1. Create a Tenant
console.log('1. Creating new Tenant: "Test Hotel A.S."');
const tenant = await prisma.tenant.create({
data: { name: 'Test Hotel A.S.' }
});
console.log(` Tenant created with ID: ${tenant.id}`);
// 2. Figure out VPN IP
const baseIp = '10.10.0.';
const lastDevice = await prisma.device.findFirst({
where: { vpnIp: { startsWith: baseIp } },
orderBy: { vpnIp: 'desc' }
});
let nextVpnIp = '10.10.0.1';
if (lastDevice && lastDevice.vpnIp) {
const lastOctet = parseInt(lastDevice.vpnIp.split('.')[3], 10);
nextVpnIp = `${baseIp}${lastOctet + 1}`;
}
console.log(`2. Allocated VPN IP for new device: ${nextVpnIp}`);
// 3. Create Device
console.log('3. Registering OPNsense device...');
const mac = '00:11:22:33:44:55';
const device = await prisma.device.create({
data: {
tenantId: tenant.id,
name: 'Test Hotel Gateway',
macAddress: mac,
vpnIp: nextVpnIp,
opnsenseUrl: 'https://192.168.1.1',
apiKey: 'testkey',
apiSecret: 'testsecret'
}
});
console.log(` Device registered successfully with MAC: ${mac}`);
// 4. Trigger VPN Generation
console.log('4. Connecting to VPN Server (10.0.1.150) via SSH to generate .ovpn config...');
try {
await vpnService.generateDeviceVpnConfig(mac, nextVpnIp);
console.log(' SUCCESS! .ovpn file and ccd config created on VPN server.');
} catch (e: any) {
console.error(' FAILED to generate VPN config:', e.message);
}
console.log('--- Test Completed ---');
}
runTest().catch(e => console.error(e)).finally(() => prisma.$disconnect());

View File

@ -1,8 +1,10 @@
import { Router } from 'express';
import { PrismaClient } from '@prisma/client';
import { VpnService } from '../services/VpnService';
const router = Router();
const prisma = new PrismaClient();
const vpnService = new VpnService();
// Helper to generate a VPN IP for a new device
async function getNextVpnIp(): Promise<string> {
@ -55,8 +57,14 @@ router.post('/', async (req, res) => {
},
});
// TODO: Actually trigger OpenVPN config generation via SSH or scripts to the VPN Server (10.0.1.150)
// generateVpnConfig(nextVpnIp, macAddress);
// 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) {

View File

@ -0,0 +1,88 @@
import { Client } from 'ssh2';
export 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 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
});
});
}
}

View File

@ -0,0 +1,94 @@
import { Client } from 'ssh2';
export class VpnService {
private host: string;
private password: string;
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: string, vpnIp: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const conn = new 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: any, signal: any) => {
conn.end();
if (code === 0) {
resolve(true);
} else {
reject(new Error(`VPN Script failed with code ${code}. Output: ${output}`));
}
}).on('data', (data: any) => {
output += data.toString();
console.log('STDOUT: ' + data);
}).stderr.on('data', (data: any) => {
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
});
});
}
}

44
backend/tsconfig.json Normal file
View File

@ -0,0 +1,44 @@
{
// 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
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}