Skip to content

Commit b85b79c

Browse files
committed
feat(api): add export endpoint for local
1 parent 3f374fd commit b85b79c

5 files changed

Lines changed: 150 additions & 3 deletions

File tree

package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"express-ws": "^5.0.2",
5959
"got": "^14.4.7",
6060
"http-z": "^7.0.0",
61+
"js-yaml": "^4.1.0",
6162
"minimist": "^1.2.8",
6263
"mqtt": "^5.13.0",
6364
"node-forge": "^1.3.1",
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { WirelessConfig } from 'models/RCS.Config.js'
2+
import Logger from '../../../Logger.js'
3+
import { NOT_FOUND_EXCEPTION, NOT_FOUND_MESSAGE } from '../../../utils/constants.js'
4+
import handleError from '../../../utils/handleError.js'
5+
import { RPSError } from '../../../utils/RPSError.js'
6+
import { type Request, type Response } from 'express'
7+
import yaml from 'js-yaml'
8+
import { encryptWithRandomKey } from '../../../utils/encryptWithRandomKey.js'
9+
10+
export async function exportProfile(req: Request, res: Response): Promise<void> {
11+
const log = new Logger('exportProfile')
12+
const { profileName, domainName } = req.params
13+
try {
14+
const result = await req.db.profiles.getByName(profileName, req.tenantId)
15+
if (result == null) {
16+
throw new RPSError(NOT_FOUND_MESSAGE('AMT', profileName), NOT_FOUND_EXCEPTION)
17+
}
18+
if (result.ciraConfigName && result.ciraConfigName !== '') {
19+
result.ciraConfigObject = await req.db.ciraConfigs.getByName(result.ciraConfigName, req.tenantId)
20+
if (result.ciraConfigObject == null) {
21+
throw new RPSError(NOT_FOUND_MESSAGE('CIRA', result.ciraConfigName), NOT_FOUND_EXCEPTION)
22+
}
23+
}
24+
if (result.activation === 'acmactivate') {
25+
const domainStuff = await req.db.domains.getByName(domainName, req.tenantId)
26+
;(result as any).domainObject = domainStuff
27+
}
28+
const wifiConfigs: any[] = []
29+
if (result.wifiConfigs && result.wifiConfigs.length > 0) {
30+
for (const wifiConfig of result.wifiConfigs) {
31+
const wifiConfigObject = await req.db.wirelessProfiles.getByName(wifiConfig.profileName, req.tenantId)
32+
if (wifiConfigObject == null) {
33+
throw new RPSError(NOT_FOUND_MESSAGE('Wireless', wifiConfig.profileName), NOT_FOUND_EXCEPTION)
34+
}
35+
wifiConfigs.push({
36+
profileName: wifiConfigObject.profileName,
37+
ssid: wifiConfigObject.ssid,
38+
priority: wifiConfig.priority,
39+
authenticationMethod: wifiConfigObject.authenticationMethod,
40+
encryptionMethod: wifiConfigObject.encryptionMethod,
41+
pskPassphrase: wifiConfigObject.pskPassphrase,
42+
ieee8021xProfileName: wifiConfigObject.ieee8021xProfileName
43+
})
44+
}
45+
}
46+
let ieee8021xConfigs: any[] = []
47+
if (result.ieee8021xProfileName && result.ieee8021xProfileName !== '') {
48+
const ieee8021xProfile = await req.db.ieee8021xProfiles.getByName(result.ieee8021xProfileName, req.tenantId)
49+
if (ieee8021xProfile == null) {
50+
throw new RPSError(NOT_FOUND_MESSAGE('802.1x', result.ieee8021xProfileName), NOT_FOUND_EXCEPTION)
51+
}
52+
ieee8021xConfigs = [
53+
{
54+
profileName: ieee8021xProfile.profileName,
55+
username: ieee8021xProfile.username,
56+
password: ieee8021xProfile.password,
57+
authenticationProtocol: ieee8021xProfile.authenticationProtocol
58+
// clientCert: ieee8021xProfile.clientCert,
59+
// caCert: ieee8021xProfile.caCert,
60+
// privateKey: ieee8021xProfile.privateKey
61+
}
62+
]
63+
}
64+
65+
// Map to Go struct shape
66+
const output = {
67+
password: result.amtPassword ?? '', // Adjust as needed
68+
tlsConfig: {
69+
//delay: result.tlsDelay ?? 3,
70+
mode: result.tlsMode ?? ''
71+
},
72+
wiredConfig: {
73+
dhcp: result.dhcpEnabled ?? true,
74+
static: !result.dhcpEnabled,
75+
ipsync: result.ipSyncEnabled ?? false,
76+
// ipaddress: result.ipAddress ?? '',
77+
// subnetmask: result.subnetMask ?? '',
78+
// gateway: result.gateway ?? '',
79+
// primarydns: result.primaryDns ?? '',
80+
// secondarydns: result.secondaryDns ?? '',
81+
ieee8021xProfileName: result.ieee8021xProfileName ?? ''
82+
},
83+
wifiConfigs,
84+
wifiSyncEnabled: result.localWifiSyncEnabled ?? false,
85+
ieee8021xConfigs,
86+
acmactivate:
87+
result.activation === 'acmactivate'
88+
? {
89+
amtPassword: result.amtPassword ?? '',
90+
provisioningCert: '',
91+
provisioningCertPwd: ''
92+
}
93+
: null,
94+
enterpriseAssistant: {
95+
eaAddress: '', //result.eaAddress ?? '',
96+
eaUsername: '', //result.eaUsername ?? '',
97+
eaPassword: '', //result.eaPassword ?? '',
98+
eaConfigured: '' //result.eaConfigured ?? false
99+
},
100+
// stopConfig: result.stopConfig ?? false,
101+
ccmactivate:
102+
result.activation === 'ccmactivate'
103+
? {
104+
amtPassword: result.amtPassword ?? ''
105+
}
106+
: null
107+
}
108+
109+
const yamlStr = yaml.dump(output)
110+
// Encrypt the YAML string and provide the key
111+
const { cipherText, key } = encryptWithRandomKey(yamlStr)
112+
res.status(200).send({
113+
filename: `${profileName}.yaml.enc`,
114+
content: cipherText,
115+
key
116+
})
117+
} catch (error) {
118+
handleError(log, profileName, req, res, error)
119+
}
120+
}

src/routes/admin/profiles/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
**********************************************************************/
55

66
import { deleteProfile } from './delete.js'
7-
87
import { Router } from 'express'
98
import { allProfiles } from './all.js'
109
import { getProfile } from './get.js'
@@ -14,12 +13,15 @@ import { amtProfileValidator, profileUpdateValidator } from './amtProfileValidat
1413
import { odataValidator } from '../odataValidator.js'
1514
import validateMiddleware from '../../../middleware/validate.js'
1615
import ifMatchMiddleware from '../../../middleware/if-match.js'
16+
import { exportProfile } from './export.js'
17+
1718
const profileRouter: Router = Router()
1819

1920
profileRouter.get('/', odataValidator(), validateMiddleware, allProfiles)
2021
profileRouter.get('/:profileName', getProfile)
2122
profileRouter.post('/', amtProfileValidator(), validateMiddleware, createProfile)
2223
profileRouter.patch('/', profileUpdateValidator(), validateMiddleware, ifMatchMiddleware, editProfile)
2324
profileRouter.delete('/:profileName', deleteProfile)
25+
profileRouter.get('/export/:profileName', exportProfile)
2426

2527
export default profileRouter

src/utils/encryptWithRandomKey.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import crypto from 'crypto'
2+
3+
/**
4+
* Encrypts a string using AES-256-GCM with a random key and random nonce, returns base64 ciphertext and key.
5+
* @param {string} plainText - The plaintext to encrypt.
6+
* @returns {{ cipherText: string, key: string }}
7+
*/
8+
export function encryptWithRandomKey(plainText: string): { cipherText: string, key: string } {
9+
// Generate a random 32-byte key (256 bits)
10+
const key = crypto.randomBytes(32)
11+
// Generate a random 12-byte nonce (GCM standard)
12+
const nonce = crypto.randomBytes(12)
13+
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
14+
const encrypted = Buffer.concat([
15+
cipher.update(plainText, 'utf8'),
16+
cipher.final()
17+
])
18+
const tag = cipher.getAuthTag()
19+
// Prepend nonce and tag to ciphertext for transport
20+
const cipherText = Buffer.concat([nonce, tag, encrypted]).toString('base64')
21+
return {
22+
cipherText,
23+
key: key.toString('base64')
24+
}
25+
}

0 commit comments

Comments
 (0)