kurulum2
This commit is contained in:
parent
6b5a6d0a71
commit
ced2c9d61b
161
backend/package-lock.json
generated
161
backend/package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.19.0",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@ -1184,6 +1185,18 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
@ -1222,6 +1235,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/aws-ssl-profiles": {
|
"node_modules/aws-ssl-profiles": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
@ -1232,6 +1251,18 @@
|
|||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
|
||||||
|
"integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.16.0",
|
||||||
|
"form-data": "^4.0.6",
|
||||||
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
@ -1454,6 +1485,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/confbox": {
|
"node_modules/confbox": {
|
||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||||
@ -1729,6 +1772,15 @@
|
|||||||
"robust-predicates": "^3.0.2"
|
"robust-predicates": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/denque": {
|
"node_modules/denque": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
@ -1877,6 +1929,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/escape-html": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@ -2055,6 +2122,26 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@ -2072,6 +2159,43 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.4",
|
||||||
|
"mime-types": "^2.1.35"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -2246,6 +2370,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
@ -2278,6 +2417,19 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.7.3",
|
"version": "0.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||||
@ -2934,6 +3086,15 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.19.0",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
@ -21,6 +21,7 @@ model Device {
|
|||||||
tenantId String
|
tenantId String
|
||||||
name String
|
name String
|
||||||
macAddress String
|
macAddress String
|
||||||
|
vpnIp String? @unique
|
||||||
ipAddress String?
|
ipAddress String?
|
||||||
opnsenseUrl String?
|
opnsenseUrl String?
|
||||||
apiKey String?
|
apiKey String?
|
||||||
|
|||||||
68
backend/src/routes/deviceRoutes.ts
Normal file
68
backend/src/routes/deviceRoutes.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Helper to generate a VPN IP for a new device
|
||||||
|
async function getNextVpnIp(): Promise<string> {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Actually trigger OpenVPN config generation via SSH or scripts to the VPN Server (10.0.1.150)
|
||||||
|
// generateVpnConfig(nextVpnIp, macAddress);
|
||||||
|
|
||||||
|
res.status(201).json(newDevice);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
32
backend/src/routes/tenantRoutes.ts
Normal file
32
backend/src/routes/tenantRoutes.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const prisma = new 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' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -17,30 +17,11 @@ app.get('/api/health', (req, res) => {
|
|||||||
res.json({ status: 'ok', message: '5651 SaaS API is running' });
|
res.json({ status: 'ok', message: '5651 SaaS API is running' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all tenants
|
import tenantRoutes from './routes/tenantRoutes';
|
||||||
app.get('/api/tenants', async (req, res) => {
|
import deviceRoutes from './routes/deviceRoutes';
|
||||||
try {
|
|
||||||
const tenants = await prisma.tenant.findMany();
|
|
||||||
res.json(tenants);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a new tenant
|
app.use('/api/tenants', tenantRoutes);
|
||||||
app.post('/api/tenants', async (req, res) => {
|
app.use('/api/devices', deviceRoutes);
|
||||||
try {
|
|
||||||
const { name } = req.body;
|
|
||||||
const newTenant = await prisma.tenant.create({
|
|
||||||
data: { name },
|
|
||||||
});
|
|
||||||
res.status(201).json(newTenant);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server is running on port ${PORT}`);
|
console.log(`Server is running on port ${PORT}`);
|
||||||
|
|||||||
100
backend/src/services/OpnsenseService.ts
Normal file
100
backend/src/services/OpnsenseService.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import https from 'https';
|
||||||
|
|
||||||
|
export class OpnsenseService {
|
||||||
|
private baseUrl: string;
|
||||||
|
private apiKey: string;
|
||||||
|
private apiSecret: string;
|
||||||
|
private client: any;
|
||||||
|
|
||||||
|
constructor(ipAddress: string, apiKey: string, apiSecret: string) {
|
||||||
|
// 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.Agent({ rejectUnauthorized: false });
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: this.baseUrl,
|
||||||
|
auth: {
|
||||||
|
username: this.apiKey,
|
||||||
|
password: this.apiSecret,
|
||||||
|
},
|
||||||
|
httpsAgent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check to test API connectivity
|
||||||
|
*/
|
||||||
|
async testConnection(): Promise<boolean> {
|
||||||
|
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: string, username: string, ipAddress: string): Promise<any> {
|
||||||
|
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: string, sessionId: string): Promise<any> {
|
||||||
|
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: any): Promise<any> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
frontend/src/app/admin/page.tsx
Normal file
30
frontend/src/app/admin/page.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export default function AdminDashboard() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-950 text-white p-8">
|
||||||
|
<header className="mb-8">
|
||||||
|
<h1 className="text-3xl font-light tracking-tight text-zinc-100">ArmoReg <span className="font-semibold">Admin</span></h1>
|
||||||
|
<p className="text-zinc-400 mt-2">SaaS Central Management Platform</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm hover:border-zinc-700 transition-colors">
|
||||||
|
<h2 className="text-xl font-medium mb-2">Tenants</h2>
|
||||||
|
<p className="text-zinc-400 text-sm mb-4">Manage customer accounts and subscriptions.</p>
|
||||||
|
<div className="text-3xl font-light">0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm hover:border-zinc-700 transition-colors">
|
||||||
|
<h2 className="text-xl font-medium mb-2">Devices</h2>
|
||||||
|
<p className="text-zinc-400 text-sm mb-4">Active OPNsense gateways connected.</p>
|
||||||
|
<div className="text-3xl font-light">0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-zinc-900 border border-zinc-800 rounded-xl p-6 shadow-sm hover:border-zinc-700 transition-colors">
|
||||||
|
<h2 className="text-xl font-medium mb-2">VPN Tunnels</h2>
|
||||||
|
<p className="text-zinc-400 text-sm mb-4">Active DPI bypass tunnels.</p>
|
||||||
|
<div className="text-3xl font-light">0</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
frontend/src/app/panel/page.tsx
Normal file
34
frontend/src/app/panel/page.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export default function TenantPanel() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-zinc-50 text-zinc-900 p-8">
|
||||||
|
<header className="mb-8 border-b border-zinc-200 pb-4">
|
||||||
|
<h1 className="text-3xl font-light tracking-tight text-zinc-800">Customer <span className="font-semibold text-blue-600">Panel</span></h1>
|
||||||
|
<p className="text-zinc-500 mt-2">Manage your Hotspot and Firewall settings</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white border border-zinc-200 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-medium">Hotspot Settings</h2>
|
||||||
|
<span className="bg-green-100 text-green-700 text-xs font-semibold px-2 py-1 rounded">Active</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-zinc-500 text-sm mb-4">Configure authentication methods (SMS, TC ID) for your guests.</p>
|
||||||
|
<button className="bg-zinc-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
|
||||||
|
Configure
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-zinc-200 rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-medium">Speed Limits</h2>
|
||||||
|
<span className="bg-blue-100 text-blue-700 text-xs font-semibold px-2 py-1 rounded">Configured</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-zinc-500 text-sm mb-4">Manage bandwidth allocation via OPNsense Traffic Shaper.</p>
|
||||||
|
<button className="bg-zinc-900 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-zinc-800 transition-colors">
|
||||||
|
Manage Limits
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
frontend/src/middleware.ts
Normal file
38
frontend/src/middleware.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - api (API routes)
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
|
||||||
|
*/
|
||||||
|
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function middleware(req: NextRequest) {
|
||||||
|
const url = req.nextUrl;
|
||||||
|
|
||||||
|
// Get hostname (e.g., admin.armoreg.com, panel.armoreg.com, localhost:3000)
|
||||||
|
const hostname = req.headers.get('host') || '';
|
||||||
|
|
||||||
|
// Determine which subdomain we are on
|
||||||
|
const isAdmin = hostname.includes('admin.armoreg.com') || hostname.startsWith('admin.localhost');
|
||||||
|
const isPanel = hostname.includes('panel.armoreg.com') || hostname.startsWith('panel.localhost');
|
||||||
|
|
||||||
|
// Rewrite to the appropriate folder inside /app
|
||||||
|
if (isAdmin) {
|
||||||
|
return NextResponse.rewrite(new URL(`/admin${url.pathname}`, req.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPanel) {
|
||||||
|
return NextResponse.rewrite(new URL(`/panel${url.pathname}`, req.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback (e.g. main landing page or redirect to panel)
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user