diff --git a/controllers/apps.js b/controllers/apps.js deleted file mode 100644 index 8fc7acd..0000000 --- a/controllers/apps.js +++ /dev/null @@ -1,352 +0,0 @@ -const asyncWrapper = require('../middleware/asyncWrapper'); -const ErrorResponse = require('../utils/ErrorResponse'); -const App = require('../models/App'); -const Config = require('../models/Config'); -const { Sequelize } = require('sequelize'); -const axios = require('axios'); -const Logger = require('../utils/Logger'); -const logger = new Logger(); -const k8s = require('@kubernetes/client-node'); - -// @desc Create new app -// @route POST /api/apps -// @access Public -exports.createApp = asyncWrapper(async (req, res, next) => { - // Get config from database - const pinApps = await Config.findOne({ - where: { key: 'pinAppsByDefault' }, - }); - - let app; - let _body = { ...req.body }; - - if (req.file) { - _body.icon = req.file.filename; - } - - if (pinApps) { - if (parseInt(pinApps.value)) { - app = await App.create({ - ..._body, - isPinned: true, - }); - } else { - app = await App.create(req.body); - } - } - - res.status(201).json({ - success: true, - data: app, - }); -}); - -// @desc Get all apps -// @route GET /api/apps -// @access Public -exports.getApps = asyncWrapper(async (req, res, next) => { - // Get config from database - const useOrdering = await Config.findOne({ - where: { key: 'useOrdering' }, - }); - const useDockerApi = await Config.findOne({ - where: { key: 'dockerApps' }, - }); - const useKubernetesApi = await Config.findOne({ - where: { key: 'kubernetesApps' }, - }); - const unpinStoppedApps = await Config.findOne({ - where: { key: 'unpinStoppedApps' }, - }); - - const orderType = useOrdering ? useOrdering.value : 'createdAt'; - let apps; - - if (useDockerApi && useDockerApi.value == 1) { - let containers = null; - - const host = await Config.findOne({ - where: { key: 'dockerHost' }, - }); - - try { - if (host.value.includes('localhost')) { - let { data } = await axios.get( - `http://${host.value}/containers/json?{"status":["running"]}`, - { - socketPath: '/var/run/docker.sock', - } - ); - containers = data; - } else { - let { data } = await axios.get( - `http://${host.value}/containers/json?{"status":["running"]}` - ); - containers = data; - } - } catch { - logger.log(`Can't connect to the docker api on ${host.value}`, 'ERROR'); - } - - if (containers) { - apps = await App.findAll({ - order: [[orderType, 'ASC']], - }); - - containers = containers.filter((e) => Object.keys(e.Labels).length !== 0); - const dockerApps = []; - for (const container of containers) { - let labels = container.Labels; - - if (!('flame.url' in labels)) { - for (const label of Object.keys(labels)) { - if (/^traefik.*.frontend.rule/.test(label)) { - // Traefik 1.x - let value = labels[label]; - if (value.indexOf('Host') !== -1) { - value = value.split('Host:')[1]; - labels['flame.url'] = 'https://' + value.split(',').join(';https://'); - } - } else if (/^traefik.*?\.rule/.test(label)) { - // Traefik 2.x - const value = labels[label]; - if (value.indexOf('Host') !== -1) { - const regex = /\`([a-zA-Z0-9\.\-]+)\`/g; - const domains = [] - while ((match = regex.exec(value)) != null) { - domains.push('http://' + match[1]); - } - if (domains.length > 0) { - labels['flame.url'] = domains.join(';'); - } - } - } - } - } - - if ( - 'flame.name' in labels && - 'flame.url' in labels && - /^app/.test(labels['flame.type']) - ) { - for (let i = 0; i < labels['flame.name'].split(';').length; i++) { - const names = labels['flame.name'].split(';'); - const urls = labels['flame.url'].split(';'); - let icons = ''; - - if ('flame.icon' in labels) { - icons = labels['flame.icon'].split(';'); - } - - dockerApps.push({ - name: names[i] || names[0], - url: urls[i] || urls[0], - icon: icons[i] || 'docker', - }); - } - } - } - - if (unpinStoppedApps && unpinStoppedApps.value == 1) { - for (const app of apps) { - await app.update({ isPinned: false }); - } - } - - for (const item of dockerApps) { - if (apps.some((app) => app.name === item.name)) { - const app = apps.filter((e) => e.name === item.name)[0]; - - if ( - item.icon === 'custom' || - (item.icon === 'docker' && app.icon != 'docker') - ) { - await app.update({ - name: item.name, - url: item.url, - isPinned: true, - }); - } else { - await app.update({ - name: item.name, - url: item.url, - icon: item.icon, - isPinned: true, - }); - } - } else { - await App.create({ - name: item.name, - url: item.url, - icon: item.icon === 'custom' ? 'docker' : item.icon, - isPinned: true, - }); - } - } - } - } - - if (useKubernetesApi && useKubernetesApi.value == 1) { - let ingresses = null; - - try { - const kc = new k8s.KubeConfig(); - kc.loadFromCluster(); - const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api); - await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => { - ingresses = res.body.items; - }); - } catch { - logger.log("Can't connect to the kubernetes api", 'ERROR'); - } - - if (ingresses) { - apps = await App.findAll({ - order: [[orderType, 'ASC']], - }); - - ingresses = ingresses.filter( - (e) => Object.keys(e.metadata.annotations).length !== 0 - ); - const kubernetesApps = []; - for (const ingress of ingresses) { - const annotations = ingress.metadata.annotations; - - if ( - 'flame.pawelmalak/name' in annotations && - 'flame.pawelmalak/url' in annotations && - /^app/.test(annotations['flame.pawelmalak/type']) - ) { - kubernetesApps.push({ - name: annotations['flame.pawelmalak/name'], - url: annotations['flame.pawelmalak/url'], - icon: annotations['flame.pawelmalak/icon'] || 'kubernetes', - }); - } - } - - if (unpinStoppedApps && unpinStoppedApps.value == 1) { - for (const app of apps) { - await app.update({ isPinned: false }); - } - } - - for (const item of kubernetesApps) { - if (apps.some((app) => app.name === item.name)) { - const app = apps.filter((e) => e.name === item.name)[0]; - await app.update({ ...item, isPinned: true }); - } else { - await App.create({ - ...item, - isPinned: true, - }); - } - } - } - } - - if (orderType == 'name') { - apps = await App.findAll({ - order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']], - }); - } else { - apps = await App.findAll({ - order: [[orderType, 'ASC']], - }); - } - - if (process.env.NODE_ENV === 'production') { - // Set header to fetch containers info every time - res.status(200).setHeader('Cache-Control', 'no-store').json({ - success: true, - data: apps, - }); - return; - } - - res.status(200).json({ - success: true, - data: apps, - }); -}); - -// @desc Get single app -// @route GET /api/apps/:id -// @access Public -exports.getApp = asyncWrapper(async (req, res, next) => { - const app = await App.findOne({ - where: { id: req.params.id }, - }); - - if (!app) { - return next( - new ErrorResponse(`App with id of ${req.params.id} was not found`, 404) - ); - } - - res.status(200).json({ - success: true, - data: app, - }); -}); - -// @desc Update app -// @route PUT /api/apps/:id -// @access Public -exports.updateApp = asyncWrapper(async (req, res, next) => { - let app = await App.findOne({ - where: { id: req.params.id }, - }); - - if (!app) { - return next( - new ErrorResponse(`App with id of ${req.params.id} was not found`, 404) - ); - } - - let _body = { ...req.body }; - - if (req.file) { - _body.icon = req.file.filename; - } - - app = await app.update(_body); - - res.status(200).json({ - success: true, - data: app, - }); -}); - -// @desc Delete app -// @route DELETE /api/apps/:id -// @access Public -exports.deleteApp = asyncWrapper(async (req, res, next) => { - await App.destroy({ - where: { id: req.params.id }, - }); - - res.status(200).json({ - success: true, - data: {}, - }); -}); - -// @desc Reorder apps -// @route PUT /api/apps/0/reorder -// @access Public -exports.reorderApps = asyncWrapper(async (req, res, next) => { - req.body.apps.forEach(async ({ id, orderId }) => { - await App.update( - { orderId }, - { - where: { id }, - } - ); - }); - - res.status(200).json({ - success: true, - data: {}, - }); -}); diff --git a/controllers/apps/createApp.js b/controllers/apps/createApp.js new file mode 100644 index 0000000..361e77e --- /dev/null +++ b/controllers/apps/createApp.js @@ -0,0 +1,33 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); +const loadConfig = require('../../utils/loadConfig'); + +// @desc Create new app +// @route POST /api/apps +// @access Public +const createApp = asyncWrapper(async (req, res, next) => { + const { pinAppsByDefault } = await loadConfig(); + + let app; + let _body = { ...req.body }; + + if (req.file) { + _body.icon = req.file.filename; + } + + if (pinAppsByDefault) { + app = await App.create({ + ..._body, + isPinned: true, + }); + } else { + app = await App.create(req.body); + } + + res.status(201).json({ + success: true, + data: app, + }); +}); + +module.exports = createApp; diff --git a/controllers/apps/deleteApp.js b/controllers/apps/deleteApp.js new file mode 100644 index 0000000..ed55729 --- /dev/null +++ b/controllers/apps/deleteApp.js @@ -0,0 +1,18 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); + +// @desc Delete app +// @route DELETE /api/apps/:id +// @access Public +const deleteApp = asyncWrapper(async (req, res, next) => { + await App.destroy({ + where: { id: req.params.id }, + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +module.exports = deleteApp; diff --git a/controllers/apps/docker/index.js b/controllers/apps/docker/index.js new file mode 100644 index 0000000..f76a9e2 --- /dev/null +++ b/controllers/apps/docker/index.js @@ -0,0 +1,4 @@ +module.exports = { + useKubernetes: require('./useKubernetes'), + useDocker: require('./useDocker'), +}; diff --git a/controllers/apps/docker/useDocker.js b/controllers/apps/docker/useDocker.js new file mode 100644 index 0000000..fcc4379 --- /dev/null +++ b/controllers/apps/docker/useDocker.js @@ -0,0 +1,148 @@ +const App = require('../../models/App'); +const axios = require('axios'); +const Logger = require('../../utils/Logger'); +const logger = new Logger(); +const loadConfig = require('../../utils/loadConfig'); + +const useDocker = async (apps) => { + const { + useOrdering: orderType, + unpinStoppedApps, + dockerHost: host, + } = await loadConfig(); + + let containers = null; + + // Get list of containers + try { + if (host.includes('localhost')) { + // Use default host + let { data } = await axios.get( + `http://${host}/containers/json?{"status":["running"]}`, + { + socketPath: '/var/run/docker.sock', + } + ); + + containers = data; + } else { + // Use custom host + let { data } = await axios.get( + `http://${host}/containers/json?{"status":["running"]}` + ); + + containers = data; + } + } catch { + logger.log(`Can't connect to the Docker API on ${host}`, 'ERROR'); + } + + if (containers) { + apps = await App.findAll({ + order: [[orderType, 'ASC']], + }); + + // Filter out containers without any annotations + containers = containers.filter((e) => Object.keys(e.Labels).length !== 0); + + const dockerApps = []; + + for (const container of containers) { + let labels = container.Labels; + + // todo + if (!('flame.url' in labels)) { + for (const label of Object.keys(labels)) { + if (/^traefik.*.frontend.rule/.test(label)) { + // Traefik 1.x + let value = labels[label]; + + if (value.indexOf('Host') !== -1) { + value = value.split('Host:')[1]; + labels['flame.url'] = + 'https://' + value.split(',').join(';https://'); + } + } else if (/^traefik.*?\.rule/.test(label)) { + // Traefik 2.x + const value = labels[label]; + + if (value.indexOf('Host') !== -1) { + const regex = /\`([a-zA-Z0-9\.\-]+)\`/g; + const domains = []; + + while ((match = regex.exec(value)) != null) { + domains.push('http://' + match[1]); + } + + if (domains.length > 0) { + labels['flame.url'] = domains.join(';'); + } + } + } + } + } + + // add each container as flame formatted app + if ( + 'flame.name' in labels && + 'flame.url' in labels && + /^app/.test(labels['flame.type']) + ) { + for (let i = 0; i < labels['flame.name'].split(';').length; i++) { + const names = labels['flame.name'].split(';'); + const urls = labels['flame.url'].split(';'); + let icons = ''; + + if ('flame.icon' in labels) { + icons = labels['flame.icon'].split(';'); + } + + dockerApps.push({ + name: names[i] || names[0], + url: urls[i] || urls[0], + icon: icons[i] || 'docker', + }); + } + } + } + + if (unpinStoppedApps) { + for (const app of apps) { + await app.update({ isPinned: false }); + } + } + + for (const item of dockerApps) { + // If app already exists, update it + if (apps.some((app) => app.name === item.name)) { + const app = apps.find((a) => a.name === item.name); + + if ( + item.icon === 'custom' || + (item.icon === 'docker' && app.icon != 'docker') + ) { + // update without overriding icon + await app.update({ + name: item.name, + url: item.url, + isPinned: true, + }); + } else { + await app.update({ + ...item, + isPinned: true, + }); + } + } else { + // else create new app + await App.create({ + ...item, + icon: item.icon === 'custom' ? 'docker' : item.icon, + isPinned: true, + }); + } + } + } +}; + +module.exports = useDocker; diff --git a/controllers/apps/docker/useKubernetes.js b/controllers/apps/docker/useKubernetes.js new file mode 100644 index 0000000..d9961cd --- /dev/null +++ b/controllers/apps/docker/useKubernetes.js @@ -0,0 +1,70 @@ +const App = require('../../../models/App'); +const k8s = require('@kubernetes/client-node'); +const Logger = require('../../../utils/Logger'); +const logger = new Logger(); +const loadConfig = require('../../../utils/loadConfig'); + +const useKubernetes = async (apps) => { + const { useOrdering: orderType, unpinStoppedApps } = await loadConfig(); + + let ingresses = null; + + try { + const kc = new k8s.KubeConfig(); + kc.loadFromCluster(); + const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api); + await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => { + ingresses = res.body.items; + }); + } catch { + logger.log("Can't connect to the Kubernetes API", 'ERROR'); + } + + if (ingresses) { + apps = await App.findAll({ + order: [[orderType, 'ASC']], + }); + + ingresses = ingresses.filter( + (e) => Object.keys(e.metadata.annotations).length !== 0 + ); + + const kubernetesApps = []; + + for (const ingress of ingresses) { + const annotations = ingress.metadata.annotations; + + if ( + 'flame.pawelmalak/name' in annotations && + 'flame.pawelmalak/url' in annotations && + /^app/.test(annotations['flame.pawelmalak/type']) + ) { + kubernetesApps.push({ + name: annotations['flame.pawelmalak/name'], + url: annotations['flame.pawelmalak/url'], + icon: annotations['flame.pawelmalak/icon'] || 'kubernetes', + }); + } + } + + if (unpinStoppedApps) { + for (const app of apps) { + await app.update({ isPinned: false }); + } + } + + for (const item of kubernetesApps) { + if (apps.some((app) => app.name === item.name)) { + const app = apps.find((a) => a.name === item.name); + await app.update({ ...item, isPinned: true }); + } else { + await App.create({ + ...item, + isPinned: true, + }); + } + } + } +}; + +module.exports = useKubernetes; diff --git a/controllers/apps/getAllApps.js b/controllers/apps/getAllApps.js new file mode 100644 index 0000000..1172e34 --- /dev/null +++ b/controllers/apps/getAllApps.js @@ -0,0 +1,52 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); +const { Sequelize } = require('sequelize'); +const loadConfig = require('../../utils/loadConfig'); + +const { useKubernetes, useDocker } = require('./docker'); + +// @desc Get all apps +// @route GET /api/apps +// @access Public +const getAllApps = asyncWrapper(async (req, res, next) => { + const { + useOrdering: orderType, + dockerApps: useDockerAPI, + kubernetesApps: useKubernetesAPI, + } = await loadConfig(); + + let apps; + + if (useDockerAPI) { + await useDocker(apps); + } + + if (useKubernetesAPI) { + await useKubernetes(apps); + } + + if (orderType == 'name') { + apps = await App.findAll({ + order: [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']], + }); + } else { + apps = await App.findAll({ + order: [[orderType, 'ASC']], + }); + } + + if (process.env.NODE_ENV === 'production') { + // Set header to fetch containers info every time + return res.status(200).setHeader('Cache-Control', 'no-store').json({ + success: true, + data: apps, + }); + } + + res.status(200).json({ + success: true, + data: apps, + }); +}); + +module.exports = getAllApps; diff --git a/controllers/apps/getSingleApp.js b/controllers/apps/getSingleApp.js new file mode 100644 index 0000000..9a06b68 --- /dev/null +++ b/controllers/apps/getSingleApp.js @@ -0,0 +1,27 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); + +// @desc Get single app +// @route GET /api/apps/:id +// @access Public +const getSingleApp = asyncWrapper(async (req, res, next) => { + const app = await App.findOne({ + where: { id: req.params.id }, + }); + + if (!app) { + return next( + new ErrorResponse( + `App with the id of ${req.params.id} was not found`, + 404 + ) + ); + } + + res.status(200).json({ + success: true, + data: app, + }); +}); + +module.exports = getSingleApp; diff --git a/controllers/apps/index.js b/controllers/apps/index.js new file mode 100644 index 0000000..01873b3 --- /dev/null +++ b/controllers/apps/index.js @@ -0,0 +1,8 @@ +module.exports = { + createApp: require('./createApp'), + getSingleApp: require('./getSingleApp'), + deleteApp: require('./deleteApp'), + updateApp: require('./updateApp'), + reorderApps: require('./reorderApps'), + getAllApps: require('./getAllApps'), +}; diff --git a/controllers/apps/reorderApps.js b/controllers/apps/reorderApps.js new file mode 100644 index 0000000..29794b3 --- /dev/null +++ b/controllers/apps/reorderApps.js @@ -0,0 +1,23 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); + +// @desc Reorder apps +// @route PUT /api/apps/0/reorder +// @access Public +const reorderApps = asyncWrapper(async (req, res, next) => { + req.body.apps.forEach(async ({ id, orderId }) => { + await App.update( + { orderId }, + { + where: { id }, + } + ); + }); + + res.status(200).json({ + success: true, + data: {}, + }); +}); + +module.exports = reorderApps; diff --git a/controllers/apps/updateApp.js b/controllers/apps/updateApp.js new file mode 100644 index 0000000..2a996fb --- /dev/null +++ b/controllers/apps/updateApp.js @@ -0,0 +1,35 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const App = require('../../models/App'); + +// @desc Update app +// @route PUT /api/apps/:id +// @access Public +const updateApp = asyncWrapper(async (req, res, next) => { + let app = await App.findOne({ + where: { id: req.params.id }, + }); + + if (!app) { + return next( + new ErrorResponse( + `App with the id of ${req.params.id} was not found`, + 404 + ) + ); + } + + let _body = { ...req.body }; + + if (req.file) { + _body.icon = req.file.filename; + } + + app = await app.update(_body); + + res.status(200).json({ + success: true, + data: app, + }); +}); + +module.exports = updateApp; diff --git a/db/index.js b/db/index.js index 34e715f..500a261 100644 --- a/db/index.js +++ b/db/index.js @@ -1,6 +1,5 @@ const { Sequelize } = require('sequelize'); const { join } = require('path'); -const fs = require('fs'); const Umzug = require('umzug'); const backupDB = require('./utils/backupDb'); diff --git a/middleware/asyncWrapper.js b/middleware/asyncWrapper.js index 11b3e52..9d99271 100644 --- a/middleware/asyncWrapper.js +++ b/middleware/asyncWrapper.js @@ -1,17 +1,7 @@ -// const asyncWrapper = foo => (req, res, next) => { -// return Promise -// .resolve(foo(req, res, next)) -// .catch(next); -// } - -// module.exports = asyncWrapper; - function asyncWrapper(foo) { return function (req, res, next) { - return Promise - .resolve(foo(req, res, next)) - .catch(next); - } + return Promise.resolve(foo(req, res, next)).catch(next); + }; } -module.exports = asyncWrapper; \ No newline at end of file +module.exports = asyncWrapper; diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js index 5db2bb2..de93c35 100644 --- a/middleware/errorHandler.js +++ b/middleware/errorHandler.js @@ -14,10 +14,14 @@ const errorHandler = (err, req, res, next) => { logger.log(error.message.split(',')[0], 'ERROR'); + if (process.env.NODE_ENV == 'development') { + console.log(err); + } + res.status(err.statusCode || 500).json({ success: false, - error: error.message || 'Server Error' - }) -} + error: error.message || 'Server Error', + }); +}; -module.exports = errorHandler; \ No newline at end of file +module.exports = errorHandler; diff --git a/routes/apps.js b/routes/apps.js index 37c0286..6f1e817 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -4,26 +4,17 @@ const upload = require('../middleware/multer'); const { createApp, - getApps, - getApp, + getAllApps, + getSingleApp, updateApp, deleteApp, - reorderApps + reorderApps, } = require('../controllers/apps'); -router - .route('/') - .post(upload, createApp) - .get(getApps); +router.route('/').post(upload, createApp).get(getAllApps); -router - .route('/:id') - .get(getApp) - .put(upload, updateApp) - .delete(deleteApp); +router.route('/:id').get(getSingleApp).put(upload, updateApp).delete(deleteApp); -router - .route('/0/reorder') - .put(reorderApps); +router.route('/0/reorder').put(reorderApps); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/utils/getExternalWeather.js b/utils/getExternalWeather.js index 1135ef7..8b2be8d 100644 --- a/utils/getExternalWeather.js +++ b/utils/getExternalWeather.js @@ -1,15 +1,9 @@ -const Config = require('../models/Config'); const Weather = require('../models/Weather'); const axios = require('axios'); +const loadConfig = require('./loadConfig'); const getExternalWeather = async () => { - // Get config from database - const config = await Config.findAll(); - - // Find and check values - const secret = config.find(pair => pair.key === 'WEATHER_API_KEY'); - const lat = config.find(pair => pair.key === 'lat'); - const long = config.find(pair => pair.key === 'long'); + const { WEATHER_API_KEY: secret, lat, long } = await loadConfig(); if (!secret) { throw new Error('API key was not found. Weather updated failed'); @@ -21,7 +15,9 @@ const getExternalWeather = async () => { // Fetch data from external API try { - const res = await axios.get(`http://api.weatherapi.com/v1/current.json?key=${secret.value}&q=${lat.value},${long.value}`); + const res = await axios.get( + `http://api.weatherapi.com/v1/current.json?key=${secret}&q=${lat},${long}` + ); // Save weather data const cursor = res.data.current; @@ -32,12 +28,12 @@ const getExternalWeather = async () => { isDay: cursor.is_day, cloud: cursor.cloud, conditionText: cursor.condition.text, - conditionCode: cursor.condition.code + conditionCode: cursor.condition.code, }); return weatherData; } catch (err) { throw new Error('External API request failed'); } -} +}; -module.exports = getExternalWeather; \ No newline at end of file +module.exports = getExternalWeather;