From: charleswrayjr Date: Fri, 5 Sep 2025 21:03:42 +0000 (-0500) Subject: Adding new routes for git, vpn, and docker. X-Git-Url: https://git.phasecustomsoft.com/static/gitweb.js?a=commitdiff_plain;h=74160349ea0990cc8249a25851fd5cec4cdabe0e;p=phs-api.git Adding new routes for git, vpn, and docker. --- diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml index d23208f..d76b16e 100755 --- a/.idea/jsLibraryMappings.xml +++ b/.idea/jsLibraryMappings.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/.idea/phs-api.iml b/.idea/phs-api.iml index d16c9d6..3ef1604 100755 --- a/.idea/phs-api.iml +++ b/.idea/phs-api.iml @@ -7,8 +7,10 @@ + + \ No newline at end of file diff --git a/app.js b/app.js index 9eb969c..b7f29d0 100755 --- a/app.js +++ b/app.js @@ -12,6 +12,9 @@ const runOnce = require ( './src/middleware/custom-startup' ); const Routes = require( './src/routes' ); const phsdb = require( './src/phsdb' ); const fs = require('fs'); +const sshConfig = { + host:'192.168.1.62', port:22, username:'git', privateKey:fs.readFileSync( '/home/node/.ssh/git-ui' ), +}; const buffer = require('buffer'); global.Blob = buffer.Blob @@ -27,6 +30,8 @@ app.use((req, res, next) => { }); global.phsdb = phsdb; +global.fs = fs; +global.sshConfig = sshConfig; // noinspection JSCheckFunctionSignatures app.use( bodyParser.json( { diff --git a/package.json b/package.json index ed77ba2..4372ab0 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "cookie-parser": "~1.4.4", "cors": "^2.8.5", "debug": "~2.6.9", + "dockerode": "^4.0.7", "express": "5.0.0", "express-compression": "^1.0.2", "http-errors": "~1.6.3", diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 0000000..bf429b6 --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,5 @@ +module.exports = { + login: (req, res, next) => { + console.log('login'); + }, +}; \ No newline at end of file diff --git a/src/controllers/docker.controller.js b/src/controllers/docker.controller.js new file mode 100644 index 0000000..475310e --- /dev/null +++ b/src/controllers/docker.controller.js @@ -0,0 +1,83 @@ +const { Client } = require( 'ssh2' ); + +module.exports = { + stopContainer:async ( req, res, next ) => { + const { containerId } = req.body; + if (!containerId) { + return res.status( 400 ).json( { error:'Container ID required' } ); + } + + try { + const container = docker.getContainer( containerId ); + await container.stop(); + res.json( { message:`Container ${ containerId } stopped successfully` } ); + } catch (error) { + res.status( 500 ).json( { error:`Failed to stop container: ${ error.message }` } ); + } + }, startContainer:async ( req, res, next ) => { + const { containerId } = req.body; + if (!containerId) { + return res.status( 400 ).json( { error:'Container ID required' } ); + } + + try { + const container = docker.getContainer( containerId ); + await container.start(); + res.json( { message:`Container ${ containerId } started successfully` } ); + } catch (error) { + res.status( 500 ).json( { error:`Failed to start container: ${ error.message }` } ); + } + }, getContainers:async ( req, res, next ) => { + try { + const { all } = req.body; + const containers = await docker.listContainers( { all } ); + res.json( { + containers:containers.map( c => ({ + id:c.Id, name:c.Names[0], state:c.State, status:c.Status, + }) ), + } ); + } catch (error) { + res.status( 500 ).json( { error:`Failed to list containers: ${ error.message }` } ); + } + }, getImages:async ( req, res, next ) => { + try { + const images = await docker.listImages(); + res.json( { + images:images.map( i => ({ + id:i.Id, names:i.RepoTags, tags:i.RepoTags, size:i.Size, created:new Date( i.Created * 1000 ).toISOString(), + }) ), + } ); + } catch (error) { + res.status( 500 ).json( { error:`Failed to list images: ${ error.message }` } ); + } + }, composeDown:async ( req, res, next ) => { + const { composeFile } = req.body; + if (!composeFile) { + return res.status( 400 ).json( { error:'Compose file path required' } ); + } + + const conn = new Client(); + conn.on( 'ready', () => { + const command = `docker compose -f ${ composeFile } down`; + conn.exec( command, ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { message:`Compose services in ${ composeFile } stopped successfully` } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + } +}; \ No newline at end of file diff --git a/src/controllers/git.controller.js b/src/controllers/git.controller.js index a5953b9..9d21f2a 100755 --- a/src/controllers/git.controller.js +++ b/src/controllers/git.controller.js @@ -1,61 +1,127 @@ -const fs = require('fs'); -const { Client } = require('ssh2'); -const sshConfig = { - host: '192.168.1.62', - port: 22, - username: 'git', - privateKey: fs.readFileSync('/home/node/.ssh/git-ui'), -}; +const { Client } = require( 'ssh2' ); module.exports = { - createRepo: (req, res, next) => { - const { name, type, user } = req.body; - if (!name.match(/^[a-zA-Z0-9_-]+$/)) { - return res.status(400).json({ error: 'Invalid repository name' }); - } - if (type === 'private' && !user) { - return res.status(400).json({ error: 'Username required for private repository' }); - } + createRepo:( req, res, next ) => { + const { name, type, user } = req.body; + if (!name.match( /^[a-zA-Z0-9_-]+$/ )) { + return res.status( 400 ).json( { error:'Invalid repository name' } ); + } + if (type === 'private' && !user) { + return res.status( 400 ).json( { error:'Username required for private repository' } ); + } + + const conn = new Client(); + conn.on( 'ready', () => { + const commands = type === 'private' ? [`mkdir /opt/git/${ name }.git`, `cd /opt/git/${ name }.git`, `git init --bare`, `chown -R ${ user }:git /opt/git/${ name }.git`, `chmod -R u+rwX,go-rwx /opt/git/${ name }.git`,] : [`mkdir /opt/git/${ name }.git`, `cd /opt/git/${ name }.git`, `git init --bare --shared=group`, `chown -R git:git /opt/git/${ name }.git`, `chmod -R g+rwX /opt/git/${ name }.git`, `find /opt/git/${ name }.git -type d -exec chmod g+s {} \\;`, `git config core.sharedRepository group`,]; - const conn = new Client(); - conn.on('ready', () => { - const commands = type === 'private' - ? [ - `mkdir /opt/git/${name}.git`, - `cd /opt/git/${name}.git`, - `git init --bare`, - `chown -R ${user}:git /opt/git/${name}.git`, - `chmod -R u+rwX,go-rwx /opt/git/${name}.git`, - ] - : [ - `mkdir /opt/git/${name}.git`, - `cd /opt/git/${name}.git`, - `git init --bare --shared=group`, - `chown -R git:git /opt/git/${name}.git`, - `chmod -R g+rwX /opt/git/${name}.git`, - `find /opt/git/${name}.git -type d -exec chmod g+s {} \\;`, - `git config core.sharedRepository group`, - ]; + conn.exec( `sudo -u git bash -c "${ commands.join( ' && ' ) }"`, ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { message:`Repository ${ name }.git created successfully` } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, getRepos:async ( req, res, next ) => { + const conn = new Client(); + conn.on( 'ready', () => { + const command = `ls -1 /opt/git | grep '\.git$' | sed 's/\.git$//'`; + conn.exec( command, ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + const repos = output.trim().split( '\n' ).map( name => ({ + name, cloneUrl:`git@192.168.1.62:/opt/git/${ name }.git`, + }) ); + res.json( { repos } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, deleteRepo:async ( req, res, next ) => { + const { name } = req.params; + if (!name.match( /^[a-zA-Z0-9_-]+$/ )) { + return res.status( 400 ).json( { error:'Invalid repository name' } ); + } - conn.exec(`sudo -u git bash -c "${commands.join(' && ')}"`, (err, stream) => { - if (err) { - conn.end(); - return res.status(500).json({ error: 'SSH command failed' }); + const conn = new Client(); + conn.on( 'ready', () => { + const command = `rm -rf /opt/git/${ name }.git`; + conn.exec( command, ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { message:`Repository ${ name } deleted successfully` } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); } - let output = ''; - stream.on('data', (data) => (output += data)); - stream.stderr.on('data', (data) => (output += data)); - stream.on('close', (code) => { - conn.end(); - if (code === 0) { - res.json({ message: `Repository ${name}.git created successfully` }); - } else { - res.status(500).json({ error: `Command failed: ${output}` }); - } - }); - }); - }).on('error', (err) => { - res.status(500).json({ error: `SSH connection failed: ${err.message}` }); - }).connect(sshConfig); + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, cloneRepo:async ( req, res, next ) => { + const { repoName, deployPath, user } = req.body; + if (!repoName || !deployPath || !user) { + return res.status( 400 ).json( { error:'Repository name, deployment path, and user are required' } ); + } + if (!repoName.match( /^[a-zA-Z0-9_-]+$/ )) { + return res.status( 400 ).json( { error:'Invalid repository name' } ); } + + const conn = new Client(); + conn.on( 'ready', () => { + const commands = [`mkdir -p ${ deployPath }`, `chown ${ user }:${ user } ${ deployPath }`, `su - ${ user } -c "cd ${ deployPath } && git clone git@localhost:/opt/git/${ repoName }.git ."`,]; + + conn.exec( commands.join( ' && ' ), ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { message:`Repository ${ repoName } cloned to ${ deployPath }` } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, }; \ No newline at end of file diff --git a/src/controllers/vpn.controller.js b/src/controllers/vpn.controller.js new file mode 100644 index 0000000..f0814d6 --- /dev/null +++ b/src/controllers/vpn.controller.js @@ -0,0 +1,99 @@ +const { Client } = require( 'ssh2' ); + +module.exports = { + createClient:( req, res, next ) => { + const { clientName, staticIp } = req.body; + if (!clientName) { + return res.status( 400 ).json( { error:'Client name required' } ); + } + if (staticIp && !staticIp.match( /^10\.8\.0\.\d{1,3}$/ )) { + return res.status( 400 ).json( { error:'Invalid static IP (must be 10.8.0.x)' } ); + } + + const conn = new Client(); + conn.on( 'ready', () => { + const commands = [`cd /etc/openvpn/easy-rsa`, `./easyrsa build-client-full ${ clientName } nopass`, `mkdir -p /etc/openvpn/client-configs`, staticIp ? `echo "ifconfig-push ${ staticIp } 255.255.255.0" > /etc/openvpn/client-configs/${ clientName }.ccd` : 'true', `/usr/share/easy-rsa/pkitool ${ clientName }`, `openssl pkcs12 -export -in pki/issued/${ clientName }.crt -inkey pki/private/${ clientName }.key -certfile pki/ca.crt -out /etc/openvpn/client-configs/${ clientName }.p12 -passout pass:`, `openvpn --genkey --secret /etc/openvpn/client-configs/${ clientName }.tls-auth`, `cat /etc/openvpn/client-template.txt | sed "s/CLIENT_NAME/${ clientName }/" > /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `cat pki/ca.crt >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `cat pki/issued/${ clientName }.crt >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `cat pki/private/${ clientName }.key >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `cat /etc/openvpn/client-configs/${ clientName }.tls-auth >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `echo "" >> /etc/openvpn/client-configs/${ clientName }.ovpn`, `cat /etc/openvpn/client-configs/${ clientName }.ovpn`,]; + + conn.exec( commands.join( ' && ' ), ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { + message:`Client ${ clientName } created${ staticIp ? ` with IP ${ staticIp }` : '' }`, + ovpn:output + } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, revokeClient:( req, res, next ) => { + const { clientName } = req.body; + if (!clientName) { + return res.status( 400 ).json( { error:'Client name required' } ); + } + + const conn = new Client(); + conn.on( 'ready', () => { + const commands = [`cd /etc/openvpn/easy-rsa`, `./easyrsa revoke ${ clientName }`, `./easyrsa gen-crl`, `cp pki/crl.pem /etc/openvpn/crl.pem`, `rm -f /etc/openvpn/client-configs/${ clientName }.ccd`, `systemctl restart openvpn@server`,]; + + conn.exec( commands.join( ' && ' ), ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + res.json( { message:`Client ${ clientName } revoked` } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + }, getClients:async ( req, res, next ) => { + const conn = new Client(); + conn.on( 'ready', () => { + const command = `cat /etc/openvpn/server/status`; + conn.exec( command, ( err, stream ) => { + if (err) { + conn.end(); + return res.status( 500 ).json( { error:'SSH command failed' } ); + } + let output = ''; + stream.on( 'data', ( data ) => (output += data) ); + stream.stderr.on( 'data', ( data ) => (output += data) ); + stream.on( 'close', ( code ) => { + conn.end(); + if (code === 0) { + const clients = output.split( '\n' ).filter( line => line.includes( 'CLIENT_LIST' ) ).map( line => { + const parts = line.split( ',' ); + return { name:parts[1], ip:parts[2], connectedSince:parts[8] }; + } ); + res.json( { clients } ); + } else { + res.status( 500 ).json( { error:`Command failed: ${ output }` } ); + } + } ); + } ); + } ).on( 'error', ( err ) => { + res.status( 500 ).json( { error:`SSH connection failed: ${ err.message }` } ); + } ).connect( sshConfig ); + } +}; \ No newline at end of file diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..805421b --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const { validateAuth } = require('../middleware/routeHelpers'); +const authController = require('../controllers/auth.controller'); + +module.exports = (passport) => { + router.post('/login', authController.login); + return router; +}; \ No newline at end of file diff --git a/src/routes/docker.routes.js b/src/routes/docker.routes.js new file mode 100644 index 0000000..e9e9668 --- /dev/null +++ b/src/routes/docker.routes.js @@ -0,0 +1,13 @@ +const express = require( 'express' ); +const router = express.Router(); +const { validateAuth } = require( '../middleware/routeHelpers' ); +const dockerController = require( '../controllers/docker.controller' ); + +module.exports = ( passport ) => { + router.post( '/start-container', dockerController.startContainer ); + router.post( '/stop-container', dockerController.stopContainer ); + router.get( '/images', dockerController.getImages ); + router.get( '/containers', dockerController.getContainers ); + router.post( '/compose-down', dockerController.composeDown ); + return router; +}; \ No newline at end of file diff --git a/src/routes/git.routes.js b/src/routes/git.routes.js index 6c80c2c..4f41df8 100755 --- a/src/routes/git.routes.js +++ b/src/routes/git.routes.js @@ -1,11 +1,14 @@ -const express = require('express'); +const express = require( 'express' ); const router = express.Router(); -const { validateAuth } = require('../middleware/routeHelpers'); -const gitController = require('../controllers/git.controller'); +const { validateAuth } = require( '../middleware/routeHelpers' ); +const gitController = require( '../controllers/git.controller' ); -module.exports = (passport) => { +module.exports = ( passport ) => { // router.use( validateAuth( passport ) ); - router.post('/create-repo', gitController.createRepo); + router.post( '/create-repo', gitController.createRepo ); + router.get( '/', gitController.getRepos ); + router.delete( '/:name', gitController.deleteRepo ); + router.get( '/clone-repo', gitController.cloneRepo ); return router; }; \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js index d1a1c0b..ab3c644 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,12 +1,10 @@ -const express = require('express'); +const express = require( 'express' ); const router = express.Router(); module.exports.APIRoutes = ( passport ) => { - /* GET home page. */ - router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); - }); - router.use( '/git', require('./git.routes')(passport) ); + router.use( '/git', require( './git.routes' )( passport ) ); + router.use( '/docker', require( './docker.routes' )( passport ) ); + router.use( '/vpn', require( './vpn.routes' )( passport ) ); return router; -} +}; diff --git a/src/routes/vpn.routes.js b/src/routes/vpn.routes.js new file mode 100644 index 0000000..48da868 --- /dev/null +++ b/src/routes/vpn.routes.js @@ -0,0 +1,11 @@ +const express = require( 'express' ); +const router = express.Router(); +const { validateAuth } = require( '../middleware/routeHelpers' ); +const vpnController = require( '../controllers/vpn.controller' ); + +module.exports = ( passport ) => { + router.post( '/create-client', validateAuth( passport ), vpnController.createClient ); + router.delete( '/revoke-client/', validateAuth( passport ), vpnController.revokeClient ); + router.get( '/clients', validateAuth( passport ), vpnController.getClients ); + return router; +}; \ No newline at end of file