portal.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. import { Readable } from 'stream';
  2. import { Permission, Container, User } from '../database/models.js';
  3. import { docker } from '../server.js';
  4. import { readFileSync } from 'fs';
  5. let hidden = '';
  6. // The actual page
  7. export const Portal = (req, res) => {
  8. let name = req.session.user;
  9. let role = req.session.role;
  10. let avatar = name.charAt(0).toUpperCase();
  11. res.render("portal", {
  12. name: name,
  13. avatar: avatar,
  14. role: role,
  15. alert: '',
  16. });
  17. }
  18. async function CardList () {
  19. let name = req.session.user;
  20. let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
  21. for (let i = 0; i < containers.length; i++) {
  22. let details = await containerInfo(containers[i].containerName);
  23. let card = await createCard(details);
  24. cardList += card;
  25. }
  26. }
  27. export const UserContainers = async (req, res) => {
  28. let cardList = '';
  29. let name = req.session.user;
  30. let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
  31. for (let i = 0; i < containers.length; i++) {
  32. if (containers[i].containerName == null) { continue; }
  33. let details = await containerInfo(containers[i].containerName);
  34. let card = await createCard(details);
  35. cardList += card;
  36. }
  37. res.send(cardList);
  38. }
  39. async function containerInfo (containerName) {
  40. let container = docker.getContainer(containerName);
  41. let info = await container.inspect();
  42. let image = info.Config.Image.split('/');
  43. let ports_list = [];
  44. try {
  45. for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
  46. let ports = {
  47. check: 'checked',
  48. external: value[0].HostPort,
  49. internal: key.split('/')[0],
  50. protocol: key.split('/')[1]
  51. }
  52. ports_list.push(ports);
  53. }
  54. } catch {
  55. // no exposed ports
  56. }
  57. let external = 0;
  58. let internal = 0;
  59. try {
  60. external = ports_list[0].external;
  61. internal = ports_list[0].internal;
  62. } catch {
  63. // no exposed ports
  64. }
  65. let details = {
  66. name: containerName,
  67. image: image,
  68. service: image[image.length - 1].split(':')[0],
  69. state: info.State.Status,
  70. external_port: external,
  71. internal_port: internal,
  72. ports: ports_list,
  73. link: 'localhost',
  74. }
  75. return details;
  76. }
  77. async function createCard (details) {
  78. if (hidden.includes(details.name)) { return;}
  79. let shortname = details.name.slice(0, 10) + '...';
  80. let trigger = 'data-hx-trigger="load, every 3s"';
  81. let state = details.state;
  82. let state_color = '';
  83. switch (state) {
  84. case 'running':
  85. state_color = 'green';
  86. break;
  87. case 'exited':
  88. state = 'stopped';
  89. state_color = 'red';
  90. trigger = 'data-hx-trigger="load"';
  91. break;
  92. case 'paused':
  93. state_color = 'orange';
  94. trigger = 'data-hx-trigger="load"';
  95. break;
  96. case 'installing':
  97. state_color = 'blue';
  98. trigger = 'data-hx-trigger="load"';
  99. break;
  100. }
  101. // if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
  102. let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
  103. card = card.replace(/AppName/g, details.name);
  104. card = card.replace(/AppShortName/g, shortname);
  105. card = card.replace(/AppIcon/g, details.service);
  106. card = card.replace(/AppState/g, state);
  107. card = card.replace(/StateColor/g, state_color);
  108. card = card.replace(/ExternalPort/g, details.external_port);
  109. card = card.replace(/InternalPort/g, details.internal_port);
  110. card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
  111. card = card.replace(/AppNameState/g, `${details.name}State`);
  112. card = card.replace(/data-trigger=""/, trigger);
  113. return card;
  114. }
  115. let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
  116. export async function addCard (name, state) {
  117. console.log(`Adding card for ${name}: ${state}`);
  118. let details = {
  119. name: name,
  120. image: name,
  121. service: name,
  122. state: 'installing',
  123. external_port: 0,
  124. internal_port: 0,
  125. ports: [],
  126. link: 'localhost',
  127. }
  128. createCard(details).then(card => {
  129. cardList += card;
  130. });
  131. }
  132. // HTMX server-side events
  133. export const SSE = async (req, res) => {
  134. res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
  135. let eventCheck = setInterval(async () => {
  136. // builds array of containers and their states
  137. containersArray = [];
  138. await docker.listContainers({ all: true }).then(containers => {
  139. containers.forEach(container => {
  140. let name = container.Names[0].replace('/', '');
  141. if (!hidden.includes(name)) { // if not hidden
  142. containersArray.push({ container: name, state: container.State });
  143. }
  144. });
  145. });
  146. if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
  147. cardList = '';
  148. newCards = '';
  149. containersArray.forEach(container => {
  150. const { container: containerName, state } = container;
  151. const existingContainer = sentArray.find(c => c.container === containerName);
  152. if (!existingContainer) {
  153. containerInfo(containerName).then(details => {
  154. createCard(details).then(card => {
  155. newCards += card;
  156. });
  157. });
  158. res.write(`event: update\n`);
  159. res.write(`data: 'update cards'\n\n`);
  160. } else if (existingContainer.state !== state) {
  161. updatesArray.push(containerName);
  162. }
  163. containerInfo(containerName).then(details => {
  164. createCard(details).then(card => {
  165. cardList += card;
  166. });
  167. });
  168. });
  169. sentArray.forEach(container => {
  170. const { container: containerName } = container;
  171. const existingContainer = containersArray.find(c => c.container === containerName);
  172. if (!existingContainer) {
  173. updatesArray.push(containerName);
  174. }
  175. });
  176. for (let i = 0; i < updatesArray.length; i++) {
  177. res.write(`event: ${updatesArray[i]}\n`);
  178. res.write(`data: 'update cards'\n\n`);
  179. }
  180. updatesArray = [];
  181. sentArray = containersArray.slice();
  182. }
  183. }, 500);
  184. req.on('close', () => {
  185. clearInterval(eventCheck);
  186. });
  187. };
  188. export const updateCards = async (req, res) => {
  189. console.log('updateCards called');
  190. res.send(newCards);
  191. newCards = '';
  192. }
  193. export const Containers = async (req, res) => {
  194. CardList();
  195. // res.send(cardList);
  196. }
  197. export const Card = async (req, res) => {
  198. let name = req.header('hx-trigger-name');
  199. console.log(`${name} requesting updated card`);
  200. // return nothing if in hidden or not found in containersArray
  201. if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
  202. res.send('');
  203. return;
  204. } else {
  205. let details = await containerInfo(name);
  206. let card = await createCard(details);
  207. res.send(card);
  208. }
  209. }
  210. function status (state) {
  211. let status = `<span class="text-yellow align-items-center lh-1">
  212. <svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
  213. ${state}
  214. </span>`;
  215. return status;
  216. }
  217. export const Logs = (req, res) => {
  218. let name = req.header('hx-trigger-name');
  219. function containerLogs (data) {
  220. return new Promise((resolve, reject) => {
  221. let logString = '';
  222. var options = { follow: false, stdout: true, stderr: false, timestamps: false };
  223. var containerName = docker.getContainer(data);
  224. containerName.logs(options, function (err, stream) {
  225. if (err) { reject(err); return; }
  226. const readableStream = Readable.from(stream);
  227. readableStream.on('data', function (chunk) {
  228. logString += chunk.toString('utf8');
  229. });
  230. readableStream.on('end', function () {
  231. resolve(logString);
  232. });
  233. });
  234. });
  235. };
  236. containerLogs(name).then((data) => {
  237. res.send(`<pre>${data}</pre> `)
  238. });
  239. }
  240. export const Action = async (req, res) => {
  241. let name = req.header('hx-trigger-name');
  242. let state = req.header('hx-trigger');
  243. let action = req.params.action;
  244. // Start
  245. if ((action == 'start') && (state == 'stopped')) {
  246. var containerName = docker.getContainer(name);
  247. containerName.start();
  248. res.send(status('starting'));
  249. } else if ((action == 'start') && (state == 'paused')) {
  250. var containerName = docker.getContainer(name);
  251. containerName.unpause();
  252. res.send(status('starting'));
  253. // Stop
  254. } else if ((action == 'stop') && (state != 'stopped')) {
  255. var containerName = docker.getContainer(name);
  256. containerName.stop();
  257. res.send(status('stopping'));
  258. // Pause
  259. } else if ((action == 'pause') && (state == 'paused')) {
  260. var containerName = docker.getContainer(name);
  261. containerName.unpause();
  262. res.send(status('starting'));
  263. } else if ((action == 'pause') && (state == 'running')) {
  264. var containerName = docker.getContainer(name);
  265. containerName.pause();
  266. res.send(status('pausing'));
  267. // Restart
  268. } else if (action == 'restart') {
  269. var containerName = docker.getContainer(name);
  270. containerName.restart();
  271. res.send(status('restarting'));
  272. // Hide
  273. } else if (action == 'hide') {
  274. let exists = await Container.findOne({ where: {name: name}});
  275. if (!exists) {
  276. const newContainer = await Container.create({ name: name, visibility: false, });
  277. } else {
  278. exists.update({ visibility: false });
  279. }
  280. hidden = await Container.findAll({ where: {visibility:false}});
  281. hidden = hidden.map((container) => container.name);
  282. res.send("ok");
  283. // Reset View
  284. } else if (action == 'reset') {
  285. await Container.update({ visibility: true }, { where: {} });
  286. hidden = await Container.findAll({ where: {visibility:false}});
  287. hidden = hidden.map((container) => container.name);
  288. res.send("ok");
  289. }
  290. }
  291. export const Modals = async (req, res) => {
  292. let name = req.header('hx-trigger-name');
  293. let id = req.header('hx-trigger');
  294. let title = name.charAt(0).toUpperCase() + name.slice(1);
  295. if (id == 'permissions') {
  296. let permissions_list = '';
  297. let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
  298. permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
  299. let users = await User.findAll({ attributes: ['username', 'UUID']});
  300. for (let i = 0; i < users.length; i++) {
  301. let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
  302. let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
  303. if (!exists) {
  304. const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
  305. }
  306. let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
  307. if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
  308. if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
  309. if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
  310. if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
  311. if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
  312. if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
  313. if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
  314. if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
  315. user_permissions = user_permissions.replace(/EntryNumber/g, i);
  316. user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
  317. user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
  318. permissions_list += user_permissions;
  319. }
  320. permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
  321. res.send(permissions_modal);
  322. return;
  323. }
  324. if (id == 'uninstall') {
  325. let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
  326. modal = modal.replace(/AppName/g, name);
  327. // let containerPermissions = await Permission.findAll({ where: {containerName: name}});
  328. res.send(modal);
  329. return;
  330. }
  331. let modal = readFileSync('./views/modals/details.html', 'utf8');
  332. let details = await containerInfo(name);
  333. modal = modal.replace(/AppName/g, details.name);
  334. modal = modal.replace(/AppImage/g, details.image);
  335. res.send(modal);
  336. }