Split apps controllers into separate files

This commit is contained in:
Paweł Malak 2021-10-22 00:42:27 +02:00
parent b7de1e3d27
commit 34279c8b8c
16 changed files with 444 additions and 398 deletions

View file

@ -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: {},
});
});

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,4 @@
module.exports = {
useKubernetes: require('./useKubernetes'),
useDocker: require('./useDocker'),
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,8 @@
module.exports = {
createApp: require('./createApp'),
getSingleApp: require('./getSingleApp'),
deleteApp: require('./deleteApp'),
updateApp: require('./updateApp'),
reorderApps: require('./reorderApps'),
getAllApps: require('./getAllApps'),
};

View file

@ -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;

View file

@ -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;

View file

@ -1,6 +1,5 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const { join } = require('path'); const { join } = require('path');
const fs = require('fs');
const Umzug = require('umzug'); const Umzug = require('umzug');
const backupDB = require('./utils/backupDb'); const backupDB = require('./utils/backupDb');

View file

@ -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) { function asyncWrapper(foo) {
return function (req, res, next) { return function (req, res, next) {
return Promise return Promise.resolve(foo(req, res, next)).catch(next);
.resolve(foo(req, res, next)) };
.catch(next);
}
} }
module.exports = asyncWrapper; module.exports = asyncWrapper;

View file

@ -14,10 +14,14 @@ const errorHandler = (err, req, res, next) => {
logger.log(error.message.split(',')[0], 'ERROR'); logger.log(error.message.split(',')[0], 'ERROR');
if (process.env.NODE_ENV == 'development') {
console.log(err);
}
res.status(err.statusCode || 500).json({ res.status(err.statusCode || 500).json({
success: false, success: false,
error: error.message || 'Server Error' error: error.message || 'Server Error',
}) });
} };
module.exports = errorHandler; module.exports = errorHandler;

View file

@ -4,26 +4,17 @@ const upload = require('../middleware/multer');
const { const {
createApp, createApp,
getApps, getAllApps,
getApp, getSingleApp,
updateApp, updateApp,
deleteApp, deleteApp,
reorderApps reorderApps,
} = require('../controllers/apps'); } = require('../controllers/apps');
router router.route('/').post(upload, createApp).get(getAllApps);
.route('/')
.post(upload, createApp)
.get(getApps);
router router.route('/:id').get(getSingleApp).put(upload, updateApp).delete(deleteApp);
.route('/:id')
.get(getApp)
.put(upload, updateApp)
.delete(deleteApp);
router router.route('/0/reorder').put(reorderApps);
.route('/0/reorder')
.put(reorderApps);
module.exports = router; module.exports = router;

View file

@ -1,15 +1,9 @@
const Config = require('../models/Config');
const Weather = require('../models/Weather'); const Weather = require('../models/Weather');
const axios = require('axios'); const axios = require('axios');
const loadConfig = require('./loadConfig');
const getExternalWeather = async () => { const getExternalWeather = async () => {
// Get config from database const { WEATHER_API_KEY: secret, lat, long } = await loadConfig();
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');
if (!secret) { if (!secret) {
throw new Error('API key was not found. Weather updated failed'); throw new Error('API key was not found. Weather updated failed');
@ -21,7 +15,9 @@ const getExternalWeather = async () => {
// Fetch data from external API // Fetch data from external API
try { 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 // Save weather data
const cursor = res.data.current; const cursor = res.data.current;
@ -32,12 +28,12 @@ const getExternalWeather = async () => {
isDay: cursor.is_day, isDay: cursor.is_day,
cloud: cursor.cloud, cloud: cursor.cloud,
conditionText: cursor.condition.text, conditionText: cursor.condition.text,
conditionCode: cursor.condition.code conditionCode: cursor.condition.code,
}); });
return weatherData; return weatherData;
} catch (err) { } catch (err) {
throw new Error('External API request failed'); throw new Error('External API request failed');
} }
} };
module.exports = getExternalWeather; module.exports = getExternalWeather;