Compare commits

...

186 commits
v0.07 ... main

Author SHA1 Message Date
lllllllillllllillll
7d0bbc27fa
Update README.md 2024-08-28 23:35:09 -07:00
lllllllillllllillll
47c9ab68ce
Merge pull request #91 from joestump/main
Add docs for environment variables
2024-07-09 19:52:59 -07:00
Joe Stump
518bc46dab
Add docs for environment variables 2024-07-08 08:56:20 -04:00
lllllllillllllillll
00216663a5
Merge pull request #86 from lllllllillllllillll/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-19 23:49:15 -07:00
dependabot[bot]
f46648e598
Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 19:56:57 +00:00
lllllllillllllillll
d905a95764
Update README.md 2024-06-16 12:45:41 -07:00
lllllllillllllillll
a67f65996e
Update compose.yaml
changed volume to /app/config
2024-06-15 02:27:08 -07:00
lllllllillllllillll
4b5ae32a97
Update README.md
fix compose volume
2024-06-15 02:23:57 -07:00
lllllllillllllillll
ee9870f554
Update README.md 2024-06-09 02:44:49 -07:00
lllllllillllllillll
8f97e17765
Merge pull request #74 from lllllllillllllillll/dev
## v0.60 (June 9th 2024) - Permissions system and import templates
2024-06-09 01:28:36 -07:00
lllllllillllllillll
b4d472f414 v0.60 release 2024-06-09 01:24:27 -07:00
lllllllillllllillll
109d9bc171 fixed details modal 2024-06-08 16:53:28 -07:00
lllllllillllllillll
842f83ea91 reduced by one line 2024-06-04 23:45:17 -07:00
lllllllillllllillll
0dae02c382 fixed details modal and dashboard icons 2024-06-04 23:32:06 -07:00
lllllllillllllillll
3080516d93 networks display 'in use' 2024-06-01 01:56:13 -07:00
lllllllillllllillll
928855d2f7 working 'new volume' button 2024-05-30 23:21:10 -07:00
lllllllillllllillll
29d630f1dd functioning 'new volume' modal 2024-05-30 00:58:19 -07:00
lllllllillllllillll
a8bf38eedb Volumes page displays bind type and status(in use) 2024-05-29 00:22:17 -07:00
lllllllillllllillll
7cf8b84169 buttons trigger from 'mousedown'
John Carmack and Theo told me to:
https://www.youtube.com/watch?v=yaMGtiPckAQ
2024-05-23 17:25:54 -07:00
lllllllillllllillll
83180e0a62 Images display 'In use' or 'Unused' 2024-05-22 01:54:52 -07:00
lllllllillllllillll
869b7b30e7 added image pull to the images page 2024-05-21 19:54:05 -07:00
lllllllillllllillll
d0db603efe updated tar to 6.2.1 2024-05-21 00:34:24 -07:00
lllllllillllllillll
774d5f4f62 added titles to activity indicators 2024-05-21 00:24:30 -07:00
lllllllillllllillll
03be1187ef empty folder fix 2024-05-20 19:10:20 -07:00
lllllllillllllillll
57f080ec0d empty folders fix 2024-05-20 12:08:37 -07:00
lllllllillllllillll
04dbeefeb9 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-05-20 00:44:20 -07:00
lllllllillllllillll
d78cd645af removed files from git 2024-05-20 00:44:00 -07:00
lllllllillllllillll
e1367b58f1
Delete templates/compose/so_github_makes_folder.txt 2024-05-20 00:37:32 -07:00
lllllllillllllillll
37fd6f320d so_github_makes_folder 2024-05-20 00:35:55 -07:00
lllllllillllllillll
6aa325ed8e v0.60 pre-release 2024-05-19 15:55:29 -07:00
lllllllillllllillll
5081100f71 added option to remove template 2024-05-19 02:31:21 -07:00
lllllllillllllillll
cec389702c fixed install 2024-05-18 13:13:40 -07:00
lllllllillllllillll
82273d1fc1 Fixed app search and search by category 2024-05-17 19:45:38 -07:00
lllllllillllllillll
fbe26cd0ed Fixed apps.js pagnation and learn more modal 2024-05-16 18:36:16 -07:00
lllllllillllllillll
1558d61dcd apps.js - dropdown displays imported json 2024-05-16 01:44:26 -07:00
lllllllillllllillll
2d08e9fa3b added alerts for duplicate templates or apps 2024-05-15 18:47:58 -07:00
lllllllillllllillll
eda852b89e added compose install 2024-05-13 21:34:42 -07:00
lllllllillllllillll
c795cac009 created compose modal. updated import instructions 2024-05-13 00:18:39 -07:00
lllllllillllllillll
c71f330b49 added compose file import 2024-05-12 01:30:37 -07:00
lllllllillllllillll
319aab60f5 added ability to view custom json templates 2024-05-06 00:54:19 -07:00
lllllllillllllillll
5581710e75 improved app search 2024-05-05 15:34:01 -07:00
lllllllillllllillll
82c134158d updated dependencies. improved AddAlert function. 2024-05-04 15:47:24 -07:00
lllllllillllllillll
2d9914c458 Added Install Alerts 2024-05-04 01:18:51 -07:00
lllllllillllllillll
cf41f07bbd Multi-user dashboard 2024-05-03 01:13:24 -07:00
lllllllillllllillll
15722b1687 First draft of permissions system 2024-04-22 19:20:13 -07:00
lllllllillllllillll
62b7e73aac Improved UI for apps.js and new template import 2024-04-14 14:56:01 -07:00
lllllllillllllillll
64ec287286 updates to portal and router 2024-04-07 15:57:17 -07:00
lllllllillllllillll
74cf69b3d3 updated router. email no longer case sensitive. 2024-04-01 15:08:02 -07:00
lllllllillllllillll
c9d7dea132 updated dependencies and updated router auth 2024-03-31 14:37:00 -07:00
lllllllillllllillll
42ca573b51 fix README 2024-03-26 23:15:51 -07:00
lllllllillllllillll
9c41839852 added auth routes 2024-03-26 23:14:39 -07:00
lllllllillllllillll
a73a89b250 mostly working permission system. 2024-03-26 20:38:57 -07:00
lllllllillllllillll
365cdde0cc Updates to permissions modal 2024-03-24 14:09:53 -07:00
lllllllillllllillll
167dd8917e reduced html inside dashboard.js 2024-03-22 11:14:10 -07:00
lllllllillllllillll
8d9eb9981a improvements for multi-user and permissions 2024-03-21 23:59:45 -07:00
lllllllillllllillll
66f273e22e updated README and minor fixes 2024-03-20 13:19:56 -07:00
lllllllillllllillll
6a352281ab import modal. added UI elements to apps.js 2024-03-19 15:20:26 -07:00
lllllllillllllillll
5c6e2a9eaa Working install cards and fixes for dashboard.js 2024-03-18 17:14:47 -07:00
lllllllillllllillll
8b8e30772f hide/reset view fixes. updated express to 4.18.3 2024-03-18 01:08:44 -07:00
lllllllillllllillll
7c5670e92b Fixed app search and install 2024-03-17 19:32:02 -07:00
lllllllillllllillll
c2f06639f5 refactored container actions 2024-03-17 12:10:16 -07:00
lllllllillllllillll
f04f08d44d Fixed install modal and install function 2024-03-17 02:20:32 -07:00
lllllllillllllillll
974d32e350 updating apps.js to use html templates 2024-03-17 01:00:11 -07:00
lllllllillllllillll
b395de3445 removed/refactored metrics interval 2024-03-15 01:01:15 -07:00
lllllllillllllillll
e78afb90ca refactoring dashboard.js / updated compose file 2024-03-14 21:12:09 -07:00
lllllllillllllillll
c9da3bd30b Fixed appCard catagories. refactoring dashboard.js 2024-03-14 00:33:34 -07:00
lllllllillllllillll
bb84828ffe fixed users.js avatars. app.js modal 2024-03-13 19:55:13 -07:00
lllllllillllllillll
12e75af9b0 dynamically generated avatars. 2024-03-13 16:32:14 -07:00
lllllllillllllillll
a841fed064 moved footer, navbar, sidebar to partials folder 2024-03-13 15:41:49 -07:00
lllllllillllllillll
308538f579 fixes for appCard and apps.js 2024-03-13 02:14:50 -07:00
lllllllillllllillll
f615a492e8 updated container charts 2024-03-13 00:41:26 -07:00
lllllllillllllillll
6213c54165 fixed container charts not displaying 2024-03-12 20:09:38 -07:00
lllllllillllllillll
ff78e24913 I think I fixed it.... 2024-03-12 17:31:04 -07:00
lllllllillllllillll
705779ec29 fix un-done changes 2024-03-12 16:44:20 -07:00
lllllllillllllillll
c9c270fd81 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-03-12 16:41:51 -07:00
lllllllillllllillll
eb952c0a50 independently updating container cards 2024-03-12 16:23:37 -07:00
lllllllillllllillll
7bf1739c52 fixed /sse_event with htmx 2.0 change 2024-03-01 01:51:06 -08:00
lllllllillllllillll
17a479be21 updated htmx to 2.0 alpha-2 2024-02-29 23:52:50 -08:00
lllllllillllllillll
7e3617f967 made sse_event look less stupid 2024-02-29 23:42:39 -08:00
lllllllillllllillll
785b54d5aa sse event can now trigger individual cards 2024-02-29 23:14:08 -08:00
lllllllillllllillll
2dc22fd75a removed files 2024-02-28 23:47:06 -08:00
lllllllillllllillll
9a994bfbf1 converting template literals into plain html. 2024-02-28 23:15:29 -08:00
lllllllillllllillll
1d7b56907c converting /components into html 2024-02-27 23:34:26 -08:00
lllllllillllllillll
cfe9660ac2 Dashboard cards are now html components. 2024-02-27 15:41:57 -08:00
lllllllillllllillll
c7d79b296c
Merge pull request #60 from lllllllillllllillll/dev
v0.40
2024-02-26 15:59:24 -08:00
lllllllillllllillll
0596793c89 date of release 2024-02-26 15:48:16 -08:00
lllllllillllllillll
575a689406 v0.40 screenshots 2024-02-26 15:39:56 -08:00
lllllllillllllillll
d288cdb205 updated image to v0.40 2024-02-26 15:30:38 -08:00
lllllllillllllillll
f97628e9cd Bumped version to v0.40. Added Podman support. 2024-02-25 00:19:31 -08:00
lllllllillllllillll
32c2301873 Container cards now display status while waiting 2024-02-24 09:20:35 -08:00
lllllllillllllillll
e294ca7089 Server metrics styling. Container action indicator 2024-02-24 08:55:47 -08:00
lllllllillllllillll
ea9ead5709 updated dependencies: systeminformation. sequelize 2024-02-23 00:46:04 -08:00
lllllllillllllillll
eb992f706e reduced server.js to 99 lines. bug fixes. 2024-02-23 00:41:54 -08:00
lllllllillllllillll
b62e209e6f Fixed issue with dashboard and improved event loop 2024-02-21 17:05:43 -08:00
lllllllillllllillll
25280ae174 Improved dashboard controller and router.
removed console.log()s.
2024-02-18 18:23:20 -08:00
lllllllillllllillll
c27f64f308 Fixed hide and resetView 2024-02-18 00:33:09 -08:00
lllllllillllllillll
a95b042960 New logo. Updated dependencies. 2024-02-17 13:32:09 -08:00
lllllllillllllillll
666f820a1f Fixed list sort on images, networks, volumes 2024-02-17 00:37:07 -08:00
lllllllillllllillll
97481b0b75 moved routes into dashboard.js 2024-02-14 01:45:29 -08:00
lllllllillllllillll
04cc1c1df3 Tweaks to event trigger. Improved Uninstall. 2024-02-11 16:50:21 -08:00
lllllllillllllillll
003db6d7d1 fixed image,volume,networks form submit.
added selectAll to permissions modal.
2024-02-09 20:06:33 -08:00
lllllllillllllillll
95dcedbdc1 fix volumes controller. update templates.json 2024-02-08 16:06:06 -08:00
lllllllillllllillll
24941d5f32 updated permissions modal 2024-02-08 03:27:15 -08:00
lllllllillllllillll
71bbb574d1 checkbox selectAll() fix 2024-02-07 23:57:49 -08:00
lllllllillllllillll
13ee350bb2 localized htmx. fixed images/volumes/networks
added hidden checkbox so forms always return an array
2024-02-07 18:13:44 -08:00
lllllllillllllillll
377ba6ae67 working 'remove' on images and networks pages 2024-02-07 01:42:20 -08:00
lllllllillllllillll
e786b32161 updated dependencies 2024-02-06 00:54:34 -08:00
lllllllillllllillll
1938d7b2fc updated images, networks, volumes forms 2024-02-06 00:35:16 -08:00
lllllllillllllillll
70ec201924 new permissions modal 2024-02-05 23:51:36 -08:00
lllllllillllllillll
883a65faae Added "Thanks" clicker to supporters page 2024-02-04 01:19:16 -08:00
lllllllillllllillll
8feb88a2a0 Fixed container stat charts 2024-02-03 22:00:17 -08:00
lllllllillllllillll
f94bd91898 modal and graph fixes. new supporters page. 2024-02-03 15:09:20 -08:00
lllllllillllllillll
f058360b19 htmx log view 2024-01-28 23:25:05 -08:00
lllllllillllllillll
5e45e084d0 fixed container charts 2024-01-28 21:03:20 -08:00
lllllllillllllillll
0f5575075e Another big rewrite 2024-01-28 00:33:50 -08:00
lllllllillllllillll
c3f10fbb7c
Merge pull request #47 from lllllllillllllillll/dev
v0.20 - The rewrite
2024-01-20 15:36:14 -08:00
lllllllillllllillll
9c79290560 added buymeacoffee 2024-01-20 15:04:50 -08:00
lllllllillllllillll
562997826f release date 2024-01-20 13:34:45 -08:00
lllllllillllllillll
109e77c56c fix network page. 2024-01-15 01:11:58 -08:00
lllllllillllllillll
0911a15ac4 visual tweaks. fixed install logging. 2024-01-14 23:57:51 -08:00
lllllllillllllillll
df242f250f css and js changes. 2024-01-14 15:33:53 -08:00
lllllllillllllillll
e6ad8dff72 updated logo. removed comments. 2024-01-14 02:23:14 -08:00
lllllllillllllillll
81a98d3f5d updated screenshots 2024-01-13 16:04:32 -08:00
lllllllillllllillll
b6faa9786f uninstalls the right app now. 2024-01-13 02:01:56 -08:00
lllllllillllllillll
5b8157a7fe v0.20-dev changes 2024-01-13 00:04:01 -08:00
lllllllillllllillll
093b27d016 fixed typo. 2024-01-12 22:55:59 -08:00
lllllllillllllillll
3fac67621a Fixed hide/resetView. Renamed app.js to server.js.
Added install and uninstall to Syslog.
2024-01-12 22:53:43 -08:00
lllllllillllllillll
40bd0b693c re-implemented the install and uninstall functions 2024-01-11 23:43:58 -08:00
lllllllillllllillll
4d78177951 merged logout with login.js. Fixed app search. 2024-01-11 17:51:26 -08:00
lllllllillllllillll
b22894f366 fixed routing for non-admin users 2024-01-11 00:05:38 -08:00
lllllllillllllillll
6d8a919d18 Portal page for regular users. Fix apps.js submit. 2024-01-10 21:31:13 -08:00
lllllllillllllillll
a105e5fbb6 updated volumes page. added new screenshots. 2024-01-09 01:29:44 -08:00
lllllllillllllillll
3e7f714115 Updated images, networks, and volumes pages. 2024-01-08 17:30:20 -08:00
lllllllillllllillll
bd34d78648 enabled rate limit, added "MB" to image size. 2024-01-08 16:15:59 -08:00
lllllllillllllillll
68cc67d5aa Fixed image size displayed on images.js 2024-01-08 14:37:24 -08:00
lllllllillllllillll
d4fed30db6 First draft of working images page 2024-01-08 13:58:37 -08:00
lllllllillllllillll
190b902090 Added rate limiter 2024-01-08 13:29:06 -08:00
lllllllillllllillll
ec3ccc110e Fixed LastLogin not updating on register. 2024-01-07 23:41:06 -08:00
lllllllillllllillll
20c987f7ba Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2024-01-07 21:16:35 -08:00
lllllllillllllillll
569df8fa1e Fixed css and js resource links 2024-01-07 21:10:13 -08:00
lllllllillllllillll
7f77605406
Delete .gitignore 2024-01-07 19:46:48 -08:00
lllllllillllllillll
0cbf9226e5 The rewrite. v0.20 2024-01-07 18:29:56 -08:00
lllllllillllllillll
2c8d9993c6 fixed* helmet 2023-12-19 01:39:27 -08:00
lllllllillllllillll
56b18cdbba added helmet. updated readme and changelog. 2023-12-19 01:22:24 -08:00
lllllllillllllillll
e99dc8470b removed extra if statements and sqlite queries.
added list.js for sorting tables.
removed Caddy Proxy Manager from docker-compose.yaml
2023-12-17 17:22:15 -08:00
lllllllillllllillll
837c21fdb8 fixed test page. 2023-12-17 12:41:13 -08:00
lllllllillllllillll
9a065a8883 created pages for images, networks, and volumes 2023-12-17 12:28:58 -08:00
lllllllillllllillll
385e2e80ee Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2023-12-16 17:03:33 -08:00
lllllllillllllillll
d49ab1a53e auth middleware, pm2, fixed missing session data 2023-12-16 16:59:51 -08:00
lllllllillllllillll
ac1356d4b9
Merge pull request #36 from lllllllillllllillll/main
sync commits
2023-12-15 18:12:09 -08:00
lllllllillllllillll
458fe2940f
put NODE_ENV: production back 2023-12-15 18:11:10 -08:00
lllllllillllllillll
bc10e26452
Merge pull request #34 from lllllllillllllillll/dev
v0.08
2023-12-15 01:31:20 -08:00
lllllllillllllillll
d4211f72c6 v0.08 2023-12-15 01:25:07 -08:00
lllllllillllllillll
f3c6e6f155 a bunch of small tweaks 2023-12-14 17:57:56 -08:00
lllllllillllllillll
9d9d5dac2e Bump sequelize from 6.35.1 to 6.35.2 2023-12-14 02:27:18 -08:00
lllllllillllllillll
8c90b496c9 removed height 2023-12-14 00:59:16 -08:00
lllllllillllllillll
1e4ff17a37 Added card styles : Full or compact. 2023-12-14 00:40:10 -08:00
lllllllillllllillll
23be19c4d3 docker pull sheild 2023-12-13 21:26:42 -08:00
lllllllillllllillll
058eadd3cd screenshot of logs 2023-12-13 18:15:35 -08:00
lllllllillllllillll
a10371d0e1 newest features 2023-12-13 01:12:38 -08:00
lllllllillllllillll
22d769bcc3 Added 'Reset View' to dashCard 2023-12-13 01:04:56 -08:00
lllllllillllllillll
23ec95c6a6 Container links now use ServerIP address 2023-12-12 21:03:37 -08:00
lllllllillllllillll
a396764880 Added 'Hide' to containers. need to build unhide. 2023-12-12 19:38:11 -08:00
lllllllillllllillll
7515592564 Added Visibility icon to dashCard 2023-12-11 19:58:53 -08:00
lllllllillllllillll
54381968a5 reverted to express-session 2023-12-11 01:32:07 -08:00
lllllllillllllillll
6ad89b9914 Merge branch 'dev' of https://github.com/lllllllillllllillll/DweebUI into dev 2023-12-11 01:29:13 -08:00
lllllllillllllillll
b7e2d6c7ca Revert back to express-sessions for now 2023-12-11 01:28:48 -08:00
lllllllillllllillll
3bf91e20c5 Replaced connect-session with cookie-session 2023-12-10 17:31:31 -08:00
lllllllillllllillll
821ece2e88 added SECRET field to register 2023-12-10 16:37:01 -08:00
lllllllillllllillll
b5114ace4f drop-down button on Apps page
Added settings icon to Apps page.
First step for compose file support.
2023-12-10 14:25:26 -08:00
lllllllillllllillll
4a626d4aab fix formatting 2023-12-09 12:53:52 -08:00
lllllllillllllillll
96d8ea7850 Reverting to Docker volumes.
I'll create seperate instructions for bind mounts.
2023-12-09 12:49:50 -08:00
lllllllillllllillll
b17af5804e
tweaks for recent pull merge 2023-12-08 21:48:46 -08:00
lllllllillllllillll
be67ed96fb
Merge pull request #31 from steveiliop56/compose-improvements
refactor(docker-compose): small improvements
2023-12-08 18:28:57 -08:00
lllllllillllllillll
f3e32765dd
Merge branch 'dev' into compose-improvements 2023-12-08 18:28:39 -08:00
lllllllillllllillll
633c9779c9 Updating compose file 2023-12-08 18:23:31 -08:00
lllllllillllllillll
c2ba8aaa89
fix conflict by deleting 2023-12-08 13:58:08 -08:00
lllllllillllllillll
dff7e384db
fix conflict by deleting 2023-12-08 13:57:42 -08:00
lllllllillllllillll
d50d0f85ab
Add files via upload 2023-12-08 13:55:30 -08:00
lllllllillllllillll
7f6370c891
Add files via upload 2023-12-08 13:54:52 -08:00
lllllllillllllillll
1f94d6154b
Update README.md 2023-12-08 01:42:26 -08:00
lllllllillllllillll
76f31e3b90
placeholder 2023-12-08 01:41:51 -08:00
Stavros
e06932a6f4
refactor(readme): fix small typing error. 2023-12-06 22:08:30 +02:00
Stavros
632bcc63f7
refactor(readme): update compose file. 2023-12-06 22:07:37 +02:00
Stavros
8dcfc16246
feat(docker-compose): add a custom network 2023-12-06 22:05:26 +02:00
Stavros
3acb605685
refactor(docker-compose): improve docker compose file 2023-12-06 22:03:03 +02:00
156 changed files with 10420 additions and 5990 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
**/db.sqlite
**/node_modules
**/screenshots
.gitignore
.github
.git
Dockerfile
docker-compose.yaml

3
.github/FUNDING.yml vendored
View file

@ -1,2 +1 @@
github: [lllllllillllllillll]
patreon: DweebUI
patreon: DweebUI

View file

@ -15,6 +15,6 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
labels:
- "🤖 Dependencies"

View file

@ -55,7 +55,7 @@ jobs:
# Build image and only publish if not a Pull Request
- name: Build and Publish Docker Image
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
timeout-minutes: 30
with:
context: .

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
**/db.sqlite
**/node_modules
**/appdata
.github
.git

View file

@ -1,3 +1,97 @@
## v0.60 (June 9th 2024) - Permissions system and import templates
* Converted JS template literals into HTML.
* Converted modals into HTML/HTMX.
* Moved functions into dashboard controller.
* New - Modal placeholder with loading spinner.
* Container cards now update independently.
* Container cards now display pending action (starting, stopping, pausing, restarting).
* User avatars are now automatically generated.
* Updated database models.
* New - Multi-user permission system.
* Refactored dashboard to support multiple users.
* New - Banner alerts.
* New - Template importing (*.yml, *.yaml, *.json).
* Improved app search.
* New - Search by category.
* Updated dependencies.
* Removed warning from the bottom of the registration page. Will be added back in a different location.
* New - admin checks, session checks, and permission checks for router.
* Added titles to activity indicators.
* Created Github Wiki.
* Added image pull to images page.
* Images and volumes display 'In use'.
* Images display tag.
* Image pull gets latest if not set.
* Updated buttons to trigger from 'mousedown' (John Carmack + Theo told me to).
* Volumes page displays type (Volume or Bind).
* Volume button is now functional.
## v0.40 (Feb 26th 2024) - HTMX rewrite
* Pages rewritten to use HTMX.
* Removed Socket.io.
* Changed view files to *.HTML instead of *.EJS.
* Removed "USER root" from Dockerfile.
* Express sessions configured to use memorystore.
* Improved chart rendering.
* Improvements to container charts.
* Created Variables page.
* Created Supporters page.
* Ability to remove images, volumes, or networks.
* Fixed list.js sorting.
* Fixed apps.js page navigation.
* Removed stackfiles from templates.json and updated some icons.
* New logo.
* Improved handling of Docker events.
* Improved dashboard responsiveness.
* Updated server metrics styles.
* Container cards display pending action.
* Container charts only rendered if container running.
* Created permissions modal.
* Podman support (untested).
* Started a new template for FOSS apps.
## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
* Changed to ES6 imports.
* Cleaned up file structure and code layout.
* Updated DweebUI logo.
* Visual tweaks to login and registration pages.
* Added .gitignore and .dockerignore files.
* Syslogs - View logs for sign-in and registration attempts. :new:
* Docker socket now uses default connection.
* Updated Users page displays 'inactive' if no sign-ins within 30 days.
* Dashboard updates now triggered by Docker events.
* Massive reduction in the amount of HTML, CSS, and JS on client side.
* Container graphs are significantly more efficent and no longer use localStorage.
* Made dark mode the default theme.
* Created intervals to allow application to idle or scale with more users.
* Pages for images, volumes, and networks. :new:
* Localized fonts.
* CORS.
* Testing with Mocha and Supertest.
* Created Portal page. :new:
## <del>v0.09 (dev)</del> dead. (It had so many problems that I essentially rewrote everything)
* Added authentication middleware to router.
* Added gzip compression.
* Added PM2.
* Added Helmet.
* Fixed missing session data.
* Reduced sqlite queries.
## v0.08 (Dec 15th 2023)
* Updates to compose file and instructions from [steveiliop56](https://github.com/steveiliop56)
* Added SECRET field to compose file as a basic security measure.
* Visibility button to hide containers or reset view.
* Container link now uses server IP address.
* More compact container card, with style options planned.
* Improved log view.
* Removed VPN, Firewall, and VNC buttons.
* Updated dependencies (Sequelize 6.35.2)
* Fixed web pages not using the "public" static folder.
* Small tweaks to router.
* Replaced the default icon shown for missing icons (docker.png).
## v0.07 (Dec 8th 2023)
* View container logs.
* Removed Redis.

View file

@ -1,21 +1,7 @@
# syntax=docker/dockerfile:1
FROM node:21-alpine
ENV NODE_ENV production
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
USER root
COPY . .
COPY . /app
RUN npm install
EXPOSE 8000
CMD node app.js
CMD node server.js

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 lllllllillllllillll
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

130
README.md
View file

@ -1,75 +1,95 @@
# DweebUI
DweebUI is a simple Docker web interface created with javascript and node.js
Pre-Pre-Pre-Pre-Pre Alpha v0.07 ( :fire: Experimental. Don't install on any servers you care about :fire: )
[![GitHub Stars](https://img.shields.io/github/stars/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
[![GitHub License](https://img.shields.io/github/license/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
[![GitHub Activity](https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI)](https://github.com/lllllllillllllillll)
* I haven't used Github very much and I'm still new to javascript.
* This is the first project I've ever released and I'm sure it's full of plenty of bugs and mistakes.
* I probably should have waited a lot longer to share this :|
<a href="https://raw.githubusercontent.com//lllllllillllllillll/DweebUI/main/screenshots/dashboard.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard.png" width="50%"/></a>
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="50%"/></a>
<h3 align="center"><img width="150" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/public/img/logo.png"></h3>
<h4 align="center">DweebUI Beta v0.60 ( :fire: Experimental :fire: )</h4>
<h3 align="center">Free and Open-Source WebUI For Managing Your Containers.</h3>
<p align="center">
<a href=""><img src="https://img.shields.io/github/stars/lllllllillllllillll/DweebUI?style=flat"/></a>
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI%2Fdev"/></a>
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/last-commit/lllllllillllllillll/DweebUI%2Fdev"/></a>
<a href="https://hub.docker.com/r/lllllllillllllillll/dweebui"><img src="https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui"/></a>
<a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE"><img src="https://img.shields.io/github/license/lllllllillllllillll/DweebUI"/></a>
<a href="https://www.reddit.com/r/dweebui"><img src="https://img.shields.io/badge/reddit-orange"/></a>
<a href="https://www.buymeacoffee.com/lllllllillllllillll"><img src="https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee"/></a>
</p>
<h3 align="center"><img width="800" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"></h3>
## Features
* [x] Dashboard provides server metrics (cpu, ram, network, disk) and container controls on a single page.
* [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
* [x] Multi-user support with permissions system.
* [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
* [x] Windows, Linux, and MacOS compatable.
* [x] Light/Dark Mode.
* [x] Mobile Friendly.
* [x] Manage your Docker networks, images, and volumes.
* [x] Easy to install app templates.
* [x] Automatically persists data in docker volumes if bind mount isn't used.
* [x] Proxy manager for Caddy. (Optional)
* [x] Partial Portainer Template Support (Network Mode, Ports, Volumes, Enviroment Variables, Labels, Commands, Restart Policy, Nvidia Hardware Acceleration).
* [x] Multi-User built-in.
* [ ] User pages: Shortcuts, Requests, Support. (planned)
* [x] Support for Windows, Linux, and MacOS.
* [ ] Import compose files. (planned)
* [x] Javascript, Node.js, and Express.
* [x] Docker Compose Support.
* [ ] Update containers (planned).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [ ] Manage your Docker networks, images, and volumes. (planned)
* [ ] Preset variables. (planned)
* [ ] VPN, VPS, and Firewall Toggles. (planned)
* [ ] Offline Mode. (planned)
* [ ] Preset variables (planned).
* [ ] Themes (planned).
## About
* I started this as a personal project to get more familiar with Javascript and Node.js, so there may be some rough edges and spaghetti code.
* I'm open to any contributions but you may want to wait until I reach v1.0 first.
* Please post issues and discussions so I know what bugs and features to focus on.
* DweebUI is a management interface and should not be directly exposed to the internet.
## Setup
* Docker compose.yaml:
Docker Compose:
```
version: "3.9"
services:
  dweebui:
    container_name: DweebUI
    image: lllllllillllllillll/dweebui:v0.07
    environment:
      NODE_ENV: production
      PORT: 8000
      # Proxy_Manager: enabled
    restart: unless-stopped
    ports:
      - 8000:8000
    volumes:
      - dweebui:/app
      - caddyfiles:/app/caddyfiles
      - /var/run/docker.sock:/var/run/docker.sock
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui
environment:
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- dweebui:/app
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
#- /run/podman/podman.sock:/var/run/docker.sock
networks:
- dweebui_net
volumes:
  dweebui:
  caddyfiles:
dweebui:
networks:
dweebui_net:
driver: bridge
```
* Using setup.sh:
```
Extract DweebUI.zip and navigate to /DweebUI
cd DweebUI
chmod +x setup.sh
sudo ./setup.sh
```
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
Compose setup:
## Credit
* Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
* Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
* You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
Configuration:
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
* `SECRET` - A shared secret used by the registration page.
## Credits
* Dockerode and dockerode-compose by Apocas: https://github.com/apocas/dockerode
* UI was built using HTML and CSS elements from https://tabler.io/
* Apps template based on Portainer template provided by Lissy93: https://github.com/Lissy93/portainer-templates
* Icons from Walkxcode with some renames and additions: https://github.com/walkxcode/dashboard-icons
## Supporters
* MM (Patreon)
* PD (Buymeacoffee)

112
app.js
View file

@ -1,112 +0,0 @@
// Express
const express = require("express");
const app = express();
const session = require("express-session");
const PORT = process.env.PORT || 8000;
// Router
const routes = require("./routes");
// Functions and variables
const { serverStats, containerList, containerStats, containerAction, containerLogs } = require('./functions/system');
let sentList, clicked;
app.locals.site_list = '';
// Configure Session
const sessionMiddleware = session({
secret: "keyboard cat",
resave: false,
saveUninitialized: false,
cookie:{
secure:false, // Only set to true if you are using HTTPS.
httpOnly:false, // Only set to true if you are using HTTPS.
maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
}
})
// Middleware
app.set('view engine', 'ejs');
app.use([
express.static("public"),
express.json(),
express.urlencoded({ extended: true }),
sessionMiddleware,
routes
]);
// Start Express server
const server = app.listen(PORT, async () => {
console.log(`App listening on port ${PORT}`);
});
// Start Socket.io
const io = require('socket.io')(server);
io.engine.use(sessionMiddleware);
io.on('connection', (socket) => {
// Set user session
const user_session = socket.request.session;
console.log(`${user_session.user} connected from ${socket.handshake.headers.host} ${socket.handshake.address}`);
// Check if a list of containers or an install card needs to be sent
if (sentList != null) { socket.emit('cards', sentList); }
if((app.locals.install != '') && (app.locals.install != null)){ socket.emit('install', app.locals.install); }
// Send server metrics
let ServerStats = setInterval(async () => {
socket.emit('metrics', await serverStats());
}, 1000);
// Send list of containers
let ContainerList = setInterval(async () => {
let cardList = await containerList();
if (sentList !== cardList) {
sentList = cardList;
app.locals.install = '';
socket.emit('cards', cardList);
}
}, 1000);
// Send container metrics
let ContainerStats = setInterval(async () => {
let stats = await containerStats();
for (let i = 0; i < stats.length; i++) {
socket.emit('containerStats', stats[i]);
}
}, 1000);
// Container controls
socket.on('clicked', (data) => {
if (clicked == true) { return; } clicked = true;
let buttonPress = {
user: socket.request.session.user,
role: socket.request.session.role,
action: data.action,
container: data.container,
state: data.state
}
containerAction(buttonPress);
clicked = false;
});
// Container logs
socket.on('logs', (data) => {
containerLogs(data.container)
.then(logs => {
socket.emit('logString', logs);
})
.catch(err => {
console.error(err);
});
});
// On disconnect
socket.on('disconnect', () => {
clearInterval(ServerStats);
clearInterval(ContainerList);
clearInterval(ContainerStats);
});
});

View file

@ -1 +0,0 @@
import ./sites/*

View file

@ -1,994 +0,0 @@
function appCard(data) {
// make data.title lowercase
let app_name = data.name || data.title.toLowerCase();
let shortened_name = "";
let shortened_desc = data.description.slice(0, 60) + "...";
let modal = app_name.replaceAll(" ", "-");
let form_id = app_name.replaceAll("-", "_");
let note = data.note ? data.note.replaceAll(". ", ".\n") : "no notes available";
let description = data.description.replaceAll(". ", ".\n") || "no description available";
let command = data.command ? data.command : "";
let command_check = command ? "checked" : "";
let privileged = data.privileged || "";
let privileged_check = privileged ? "checked" : "";
let repository = data.repository || "";
let source = data.image || "";
// if data.network is set to host, bridge, or docker set the radio button to checked
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
if (data.network == 'host') {
net_host = 'checked';
} else if (data.network) {
net_bridge = 'checked';
net_name = data.network;
} else {
net_docker = 'checked';
}
if (data.title.length > 28) {
shortened_name = (data.title).slice(0, 25) + "...";
}
else {
shortened_name = data.title;
}
if (repository != "") {
source = (`${repository.url}/raw/master/${repository.stackfile}`);
}
function CatagoryColor(category) {
switch (category) {
case 'Other':
return '<span class="badge bg-blue-lt">Other</span> ';
case 'Productivity':
return '<span class="badge bg-blue-lt">Productivity</span> ';
case 'Tools':
return '<span class="badge bg-blue-lt">Tools</span> ';
case 'Dashboard':
return '<span class="badge bg-blue-lt">Dashboard</span> ';
case 'Communication':
return '<span class="badge bg-azure-lt">Communication</span> ';
case 'Media':
return '<span class="badge bg-azure-lt">Media</span> ';
case 'CMS':
return '<span class="badge bg-azure-lt">CMS</span> ';
case 'Monitoring':
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
case 'LDAP':
return '<span class="badge bg-purple-lt">LDAP</span> ';
case 'Arr':
return '<span class="badge bg-purple-lt">Arr</span> ';
case 'Database':
return '<span class="badge bg-red-lt">Database</span> ';
case 'Paid':
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
case 'Gaming':
return '<span class="badge bg-pink-lt">Gaming</span> ';
case 'Finance':
return '<span class="badge bg-orange-lt">Finance</span> ';
case 'Networking':
return '<span class="badge bg-yellow-lt">Networking</span> ';
case 'Authentication':
return '<span class="badge bg-lime-lt">Authentication</span> ';
case 'Development':
return '<span class="badge bg-green-lt">Development</span> ';
case 'Media Server':
return '<span class="badge bg-teal-lt">Media Server</span> ';
case 'Downloaders':
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
default:
return ''; // default to other if the category is not recognized
}
}
// set data.catagories to 'other' if data.catagories is empty or undefined
if (data.categories == null || data.categories == undefined || data.categories == '') {
data.categories = ['Other'];
}
let categories = '';
for (let i = 0; i < data.categories.length; i++) {
categories += CatagoryColor(data.categories[i]);
}
if (data.restart_policy == null) {
data.restart_policy = 'unless-stopped';
}
let ports_data = [], volumes_data = [], env_data = [], label_data = [];
for (let i = 0; i < 12; i++) {
// Get port details
try {
let ports = data.ports[i];
let port_check = ports ? "checked" : "";
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
// Get volume details
try {
let volumes = data.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
}
// Get environment details
try {
let env = data.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";
env_data.push({
check: env_check,
default: env_default,
description: env_description,
label: env_label,
name: env_name
});
} catch {
env_data.push({
check: "",
default: "",
description: "",
label: "",
name: ""
});
}
// Get label details
try {
let label = data.labels[i];
let label_check = "";
let label_name = label.name ? label.name : "";
let label_value = label.value ? label.value : "";
label_data.push({
check: label_check,
name: label_name,
value: label_value
});
} catch {
label_data.push({
check: "",
name: "",
value: ""
});
}
}
return `
<div class="col-md-6 col-lg-3">
<div class="card">
<div class="card-body p-4 text-center">
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"></img></span>
<h3 class="m-0 mb-1"><a href="#">${shortened_name}</a></h3>
<div class="text-secondary">${shortened_desc}</div>
<div class="mt-3">
${categories}
</div>
</div>
<div class="d-flex">
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info"><!-- Download SVG icon from http://tabler-icons.io/i/mail -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" 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="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
  Learn More
</a>
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-install"><!-- Download SVG icon from http://tabler-icons.io/i/phone -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" 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="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
  Install
</a>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${modal}-info" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="modal-title">${data.title}</div>
<div>${description}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${modal}-install" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Install ${data.title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre class="text-secondary">${note}</pre>
<form action="/install" name="${form_id}_install" id="${form_id}_install" method="POST">
<div class="row mb-3 align-items-end">
<div class="col-lg-6">
<label class="form-label">Container Name: </label>
<input type="text" class="form-control" name="service_name" value="${app_name}" hidden/>
<input type="text" class="form-control" name="name" value="${app_name}"/>
</div>
<div class="col-lg-3">
<label class="form-label">Image: </label>
<input type="text" class="form-control" name="image" value="${source}"/>
</div>
<div class="col-lg-3">
<label class="form-label">Restart Policy: </label>
<select class="form-select" name="restart_policy">
<option value="${data.restart_policy}" selected hidden>${data.restart_policy}</option>
<option value="unless-stopped">unless-stopped</option>
<option value="on-failure">on-failure</option>
<option value="never">never</option>
<option value="always">always</option>
</select>
</div>
</div>
<label class="form-label">Network Mode</label>
<div class="form-selectgroup-boxes row mb-3">
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" ${net_host}>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Host Network</span>
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="${net_name}" class="form-selectgroup-input" ${net_bridge}>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
<span class="d-block text-secondary">Containers can communicate using names.</span>
</span>
</span>
</label>
</div>
<div class="col">
<label class="form-selectgroup-item">
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" ${net_docker}>
<span class="form-selectgroup-label d-flex align-items-center p-3">
<span class="me-3">
<span class="form-selectgroup-check"></span>
</span>
<span class="form-selectgroup-label-content">
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
</span>
</span>
</label>
</div>
</div>
<div class="accordion" id="${modal}-accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="heading-1">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
Ports
</button>
</h2>
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port0" type="checkbox" ${ports_data[0].check}>
</div>
<div class="col">
<label class="form-label">External Port</label>
<input type="text" class="form-control" name="port_0_external" value="${ports_data[0].external}"/>
</div>
<div class="col">
<label class="form-label">Internal Port</label>
<input type="text" class="form-control" name="port_0_internal" value="${ports_data[0].internal}"/>
</div>
<div class="col-lg-2">
<label class="form-label">Protocol</label>
<select class="form-select" name="port_0_protocol">
<option value="${ports_data[0].protocol}" selected hidden>${ports_data[0].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port1" type="checkbox" ${ports_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_external" value="${ports_data[1].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_1_internal" value="${ports_data[1].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_1_protocol">
<option value="${ports_data[1].protocol}" selected hidden>${ports_data[1].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port2" type="checkbox" ${ports_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_external" value="${ports_data[2].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_2_internal" value="${ports_data[2].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_2_protocol">
<option value="${ports_data[2].protocol}" selected hidden>${ports_data[2].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port3" type="checkbox" ${ports_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_external" value="${ports_data[3].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_3_internal" value="${ports_data[3].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_3_protocol">
<option value="${ports_data[3].protocol}" selected hidden>${ports_data[3].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port4" type="checkbox" ${ports_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_external" value="${ports_data[4].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_4_internal" value="${ports_data[4].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_4_protocol">
<option value="${ports_data[4].protocol}" selected hidden>${ports_data[4].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="port5" type="checkbox" ${ports_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_external" value="${ports_data[5].external}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="port_5_internal" value="${ports_data[5].internal}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="port_5_protocol">
<option value="${ports_data[5].protocol}" selected hidden>${ports_data[5].protocol}</option>
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-2">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
Volumes
</button>
</h2>
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume0" type="checkbox" ${volumes_data[0].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_bind" value="${volumes_data[0].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_0_container" value="${volumes_data[0].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_0_readwrite">
<option value="${volumes_data[0].readwrite}" selected hidden>${volumes_data[0].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume1" type="checkbox" ${volumes_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_bind" value="${volumes_data[1].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_1_container" value="${volumes_data[1].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_1_readwrite">
<option value="${volumes_data[1].readwrite}" selected hidden>${volumes_data[1].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume2" type="checkbox" ${volumes_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_bind" value="${volumes_data[2].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_2_container" value="${volumes_data[2].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_2_readwrite">
<option value="${volumes_data[2].readwrite}" selected hidden>${volumes_data[2].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume3" type="checkbox" ${volumes_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_bind" value="${volumes_data[3].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_3_container" value="${volumes_data[3].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_3_readwrite">
<option value="${volumes_data[3].readwrite}" selected hidden>${volumes_data[3].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume4" type="checkbox" ${volumes_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_bind" value="${volumes_data[4].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_4_container" value="${volumes_data[4].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_4_readwrite">
<option value="${volumes_data[4].readwrite}" selected hidden>${volumes_data[4].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="volume5" type="checkbox" ${volumes_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_bind" value="${volumes_data[5].bind}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="volume_5_container" value="${volumes_data[5].container}"/>
</div>
<div class="col-lg-2">
<select class="form-select" name="volume_5_readwrite">
<option value="${volumes_data[5].readwrite}" selected hidden>${volumes_data[5].readwrite}</option>
<option value="rw">rw</option>
<option value="ro">ro</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-3">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
Environment Variables
</button>
</h2>
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env0" ${env_data[0].check}>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="env_0_name" value="${env_data[0].name}"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="env_0_default" value="${env_data[0].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env1" ${env_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_name" value="${env_data[1].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_1_default" value="${env_data[1].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env2" ${env_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_name" value="${env_data[2].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_2_default" value="${env_data[2].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env3" ${env_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_name" value="${env_data[3].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_3_default" value="${env_data[3].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env4" ${env_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_name" value="${env_data[4].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_4_default" value="${env_data[4].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env5" ${env_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_name" value="${env_data[5].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_5_default" value="${env_data[5].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env6" ${env_data[6].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_name" value="${env_data[6].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_6_default" value="${env_data[6].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env7" ${env_data[7].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_name" value="${env_data[7].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_7_default" value="${env_data[7].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env8" ${env_data[8].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_name" value="${env_data[8].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_8_default" value="${env_data[8].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env9" ${env_data[9].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_name" value="${env_data[9].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_9_default" value="${env_data[9].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env10" ${env_data[10].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_name" value="${env_data[10].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_10_default" value="${env_data[10].default}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="env11" ${env_data[11].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_name" value="${env_data[11].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="env_11_default" value="${env_data[11].default}"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-4">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
Labels
</button>
</h2>
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label0" ${label_data[0].check}>
</div>
<div class="col">
<label class="form-label">Variable</label>
<input type="text" class="form-control" name="label_0_name" value="${label_data[0].name}"/>
</div>
<div class="col">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="label_0_value" value="${label_data[0].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label1" ${label_data[1].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_name" value="${label_data[1].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_1_value" value="${label_data[1].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label2" ${label_data[2].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_name" value="${label_data[2].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_2_value" value="${label_data[2].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label3" ${label_data[3].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_name" value="${label_data[3].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_3_value" value="${label_data[3].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label4" ${label_data[4].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_name" value="${label_data[4].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_4_value" value="${label_data[4].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label5" ${label_data[5].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_name" value="${label_data[5].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_5_value" value="${label_data[5].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label6" ${label_data[6].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_name" value="${label_data[6].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_6_value" value="${label_data[6].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label7" ${label_data[7].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_name" value="${label_data[7].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_7_value" value="${label_data[7].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label8" ${label_data[8].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_name" value="${label_data[8].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_8_value" value="${label_data[8].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label9" ${label_data[9].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_name" value="${label_data[9].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_9_value" value="${label_data[9].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label10" ${label_data[10].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_name" value="${label_data[10].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_10_value" value="${label_data[10].value}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" type="checkbox" name="label11" ${label_data[11].check}>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_name" value="${label_data[11].name}"/>
</div>
<div class="col">
<input type="text" class="form-control" name="label_11_value" value="${label_data[11].value}"/>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="heading-5">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
Extras
</button>
</h2>
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
<div class="accordion-body pt-0">
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="command_check" type="checkbox" ${command_check}>
</div>
<div class="col">
<label class="form-label">Command</label>
<input type="text" class="form-control" name="command" value="${command}"/>
</div>
</div>
<div class="row mb-1 align-items-end">
<div class="col-auto">
<input class="form-check-input" name="privileged" type="checkbox" ${privileged_check}>
</div>
<div class="col">
<label class="form-label">Privileged Mode</label>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<input type="submit" form="${form_id}_install" class="btn btn-success" value="Install"/>
</div>
</div>
</div>
</div>`;
}
module.exports = { appCard };

File diff suppressed because it is too large Load diff

View file

@ -1,18 +0,0 @@
function siteCard(type, domain, host, port, id) {
let site = `<tr>`
site += `<td><input class="form-check-input m-0 align-middle" name="select${id}" value="${domain}" type="checkbox" aria-label="Select invoice"></td>`
site += `<td><span class="text-muted">${id}</span></td>`
site += `<td><a href="https://${domain}" class="text-reset" tabindex="-1" target="_blank">${domain}</a></td>`
site += `<td>${type}</td>`
site += `<td>${host}</td>`
site += `<td>${port}</td>`
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
site += `<td><span class="badge bg-success me-1"></span> Enabled</td>`
site += `<td class="text-end"><a class="btn" href="#"> Edit </a></td>`
site += `</tr>`
return site;
}
module.exports = { siteCard };

View file

@ -1,19 +1,27 @@
version: "3.9"
services:
dweebui:
container_name: DweebUI
image: lllllllillllllillll/dweebui:v0.07
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.60
environment:
NODE_ENV: production
PORT: 8000
# Proxy_Manager: enabled
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- dweebui:/app
- caddyfiles:/app/caddyfiles
- dweebui:/app/config
# Docker socket
- /var/run/docker.sock:/var/run/docker.sock
# Podman socket
#- /run/podman/podman.sock:/var/run/docker.sock
networks:
- dweebui_net
volumes:
dweebui:
caddyfiles:
networks:
dweebui_net:
driver: bridge

View file

@ -1,22 +1,20 @@
const User = require('../database/UserModel');
import { User } from "../database/models.js";
export const Account = async (req, res) => {
let user = await User.findOne({ where: { UUID: req.session.UUID }});
res.render("account", {
first_name: user.name,
last_name: user.name,
name: user.name,
id: user.id,
email: user.email,
role: user.role,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
exports.Account = async function(req, res) {
if (req.session.user) {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
// Render the home page
res.render("pages/account", {
first_name: user.first_name,
last_name: user.last_name,
name: user.first_name + ' ' + user.last_name,
id: user.id,
email: user.email,
role: user.role,
avatar: user.avatar,
isLoggedIn: true
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}

View file

@ -1,192 +1,587 @@
const User = require('../database/UserModel');
const { appCard } = require('../components/appCard')
const { dashCard } = require('../components/dashCard');
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
import { parse } from 'yaml';
import multer from 'multer';
import AdmZip from 'adm-zip';
const { install, uninstall } = require('../functions/package_manager');
const upload = multer({storage: multer.diskStorage({
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
filename: function (req, file, cb) { cb(null, file.originalname) },
})});
// import { install, uninstall } from '../functions/package_manager';
let alert = '';
let templates_global = '';
let json_templates = '';
let remove_button = '';
const templates_json = require('../templates.json');
let templates = templates_json.templates;
// sort templates alphabetically
templates = templates.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
});
export const Apps = async (req, res) => {
let page = Number(req.params.page) || 1;
let template_param = req.params.template || 'default';
exports.Apps = async function(req, res) {
if (req.session.role == "admin") {
if ((template_param != 'default') && (template_param != 'compose')) {
remove_button = `<a href="/remove_template/${template_param}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
} else {
remove_button = '';
}
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let page = Number(req.query.page) || 1;
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = Math.ceil(templates.length / 28);
let prev = '/apps?page=' + (page - 1);
let next = '/apps?page=' + (page + 1);
if (page == 1) {
prev = '/apps?page=' + (page);
}
if (page == last_page) {
next = '/apps?page=' + (page);
}
let apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let app_card = appCard(templates[i]);
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
} else {
// Redirect to the login page
res.redirect("/login");
json_templates = '';
let json_files = readdirSync('templates/json/');
for (let i = 0; i < json_files.length; i++) {
if (json_files[i] != 'default.json') {
let filename = json_files[i].split('.')[0];
let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
json_templates += link;
}
}
}
let apps_list = '';
let app_count = '';
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = '';
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
exports.searchApps = async function(req, res) {
if (req.session.role == "admin") {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let page = Number(req.query.page) || 1;
let list_start = (page - 1) * 28;
let list_end = (page * 28);
let last_page = Math.ceil(templates.length / 28);
let prev = '/apps?page=' + (page - 1);
let next = '/apps?page=' + (page + 1);
if (page == 1) {
prev = '/apps?page=' + (page);
}
if (page == last_page) {
next = '/apps?page=' + (page);
}
let apps_list = '';
let search_results = [];
let search = req.body.search;
// split value of search into an array of words
search = search.split(' ');
try {console.log(search[0]);} catch (error) {}
try {console.log(search[1]);} catch (error) {}
try {console.log(search[2]);} catch (error) {}
function searchTemplates(word) {
for (let i = 0; i < templates.length; i++) {
if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
search_results.push(templates[i]);
}
}
// console.log(search_results);
}
searchTemplates(search);
for (let i = 0; i < search_results.length; i++) {
let app_card = appCard(search_results[i]);
apps_list += app_card;
}
// Render the home page
res.render("pages/apps", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
list_start: list_start + 1,
list_end: list_end,
app_count: templates.length,
prev: prev,
next: next,
apps_list: apps_list
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}
let prev = '/apps/' + (page - 1) + '/' + template_param;
let next = '/apps/' + (page + 1) + '/' + template_param;
if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
exports.Install = async function (req, res) {
if (template_param == 'compose') {
let compose_files = readdirSync('templates/compose/');
if (req.session.role == "admin") {
app_count = compose_files.length;
last_page = Math.ceil(compose_files.length/28);
console.log(`Starting install for: ${req.body.name}`)
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
install(req.body);
let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services)
let container = compose_data.services[service_name].container_name;
let image = compose_data.services[service_name].image;
let container_info = {
name: req.body.name,
service: req.body.service_name,
state: 'installing',
image: req.body.image,
restart_policy: req.body.restart_policy
}
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
appCard = appCard.replace(/AppName/g, service_name);
appCard = appCard.replace(/AppShortName/g, service_name);
appCard = appCard.replace(/AppDesc/g, 'Compose File');
appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
appCard = appCard.replace(/AppType/g, 'compose');
apps_list += appCard;
});
} else {
let installCard = dashCard(container_info);
let template_file = readFileSync(`./templates/json/${template_param}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
app_count = templates.length;
req.app.locals.install = installCard;
templates_global = templates;
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
apps_list = '';
for (let i = list_start; i < list_end && i < templates.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
let name = templates[i].name || templates[i].title.toLowerCase();
let title = templates[i].title || templates[i].name;
let desc = templates[i].description.slice(0, 60) + "...";
let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
let image = templates[i].image;
let logo = templates[i].logo;
let categories = '';
// set data.catagories to 'other' if data.catagories is empty or undefined
if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
templates[i].categories = ['Other'];
}
// loop through the categories and add the badge to the card
for (let j = 0; j < templates[i].categories.length; j++) {
categories += CatagoryColor(templates[i].categories[j]);
}
appCard = appCard.replace(/AppName/g, name);
appCard = appCard.replace(/AppTitle/g, title);
appCard = appCard.replace(/AppShortName/g, name);
appCard = appCard.replace(/AppDesc/g, desc);
appCard = appCard.replace(/AppLogo/g, logo);
appCard = appCard.replace(/AppCategories/g, categories);
appCard = appCard.replace(/AppType/g, 'json');
apps_list += appCard;
}
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
list_start: list_start + 1,
list_end: list_end,
app_count: app_count,
prev: prev,
next: next,
apps_list: apps_list,
alert: alert,
template_list: '',
json_templates: json_templates,
pages: pages,
remove_button: remove_button
});
alert = '';
}
export const removeTemplate = async (req, res) => {
let template = req.params.template;
unlinkSync(`templates/json/${template}.json`);
res.redirect('/apps');
}
export const appSearch = async (req, res) => {
exports.Uninstall = async function (req, res) {
let search = req.body.search;
let page = Number(req.params.page) || 1;
let template_param = req.params.template || 'default';
let template_file = readFileSync(`./templates/json/${template_param}.json`);
let templates = JSON.parse(template_file).templates;
templates = templates.sort((a, b) => {
if (a.name < b.name) { return -1; }
});
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
let list_start = (page-1)*28;
let list_end = (page*28);
let last_page = Math.ceil(templates.length/28);
let prev = '/apps/' + (page-1);
let next = '/apps/' + (page+1);
if (page == 1) { prev = '/apps/' + (page); }
if (page == last_page) { next = '/apps/' + (page); }
let apps_list = '';
let results = [];
let [cat_1, cat_2, cat_3] = ['','',''];
function searchTemplates(terms) {
terms = terms.toLowerCase();
for (let i = 0; i < templates.length; i++) {
if (templates[i].categories) {
if (templates[i].categories[0]) {
cat_1 = (templates[i].categories[0]).toLowerCase();
}
if (templates[i].categories[1]) {
cat_2 = (templates[i].categories[1]).toLowerCase();
}
if (templates[i].categories[2]) {
cat_3 = (templates[i].categories[2]).toLowerCase();
}
}
if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
results.push(templates[i]);
}
}
}
searchTemplates(search);
for (let i = 0; i < results.length; i++) {
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
let name = results[i].name || results[i].title.toLowerCase();
let desc = results[i].description.slice(0, 60) + "...";
let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
let image = results[i].image;
let logo = results[i].logo;let categories = '';
// set data.catagories to 'other' if data.catagories is empty or undefined
if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
results[i].categories = ['Other'];
}
// loop through the categories and add the badge to the card
for (let j = 0; j < results[i].categories.length; j++) {
categories += CatagoryColor(results[i].categories[j]);
}
appCard = appCard.replace(/AppName/g, name);
appCard = appCard.replace(/AppShortName/g, name);
appCard = appCard.replace(/AppDesc/g, desc);
appCard = appCard.replace(/AppLogo/g, logo);
appCard = appCard.replace(/AppCategories/g, categories);
appCard = appCard.replace(/AppType/g, 'json');
apps_list += appCard;
}
res.render("apps", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
list_start: list_start + 1,
list_end: list_end,
app_count: results.length,
prev: prev,
next: next,
apps_list: apps_list,
alert: alert,
template_list: '',
json_templates: json_templates,
pages: pages,
remove_button: remove_button
});
}
function CatagoryColor(category) {
switch (category) {
case 'Other':
return '<span class="badge bg-blue-lt">Other</span> ';
case 'Productivity':
return '<span class="badge bg-blue-lt">Productivity</span> ';
case 'Tools':
return '<span class="badge bg-blue-lt">Tools</span> ';
case 'Dashboard':
return '<span class="badge bg-blue-lt">Dashboard</span> ';
case 'Communication':
return '<span class="badge bg-azure-lt">Communication</span> ';
case 'Media':
return '<span class="badge bg-azure-lt">Media</span> ';
case 'CMS':
return '<span class="badge bg-azure-lt">CMS</span> ';
case 'Monitoring':
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
case 'LDAP':
return '<span class="badge bg-purple-lt">LDAP</span> ';
case 'Arr':
return '<span class="badge bg-purple-lt">Arr</span> ';
case 'Database':
return '<span class="badge bg-red-lt">Database</span> ';
case 'Paid':
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
case 'Gaming':
return '<span class="badge bg-pink-lt">Gaming</span> ';
case 'Finance':
return '<span class="badge bg-orange-lt">Finance</span> ';
case 'Networking':
return '<span class="badge bg-yellow-lt">Networking</span> ';
case 'Authentication':
return '<span class="badge bg-lime-lt">Authentication</span> ';
case 'Development':
return '<span class="badge bg-green-lt">Development</span> ';
case 'Media Server':
return '<span class="badge bg-teal-lt">Media Server</span> ';
case 'Downloaders':
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
default:
return ''; // default to other if the category is not recognized
}
}
export const InstallModal = async (req, res) => {
let input = req.header('hx-trigger-name');
let type = req.header('hx-trigger');
if (type == 'compose') {
let compose = readFileSync(`templates/compose/${input}/compose.yaml`, 'utf8');
let modal = readFileSync('./views/modals/compose.html', 'utf8');
modal = modal.replace(/AppName/g, input);
modal = modal.replace(/COMPOSE_CONTENT/g, compose);
res.send(modal);
return;
} else {
let result = templates_global.find(t => t.name == input);
let name = result.name || result.title.toLowerCase();
let short_name = name.slice(0, 25) + "...";
let desc = result.description.replaceAll(". ", ".\n") || "no description available";
let short_desc = desc.slice(0, 60) + "...";
let modal_name = name.replaceAll(" ", "-");
let form_id = name.replaceAll("-", "_");
let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
let command = result.command ? result.command : "";
let command_check = command ? "checked" : "";
let privileged = result.privileged || "";
let privileged_check = privileged ? "checked" : "";
let repository = result.repository || "";
let image = result.image || "";
let net_host, net_bridge, net_docker = '';
let net_name = 'AppBridge';
let restart_policy = result.restart_policy || 'unless-stopped';
if (req.session.role == "admin") {
switch (result.network) {
case 'host':
net_host = 'checked';
break;
case 'bridge':
net_bridge = 'checked';
net_name = result.network;
break;
default:
net_docker = 'checked';
}
if (repository != "") {
image = (`${repository.url}/raw/master/${repository.stackfile}`);
}
if (req.body.confirm == 'Yes') {
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
uninstall(req.body);
for (let i = 0; i < 12; i++) {
// Get port details
try {
let ports = result.ports[i];
let port_check = ports ? "checked" : "";
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
// remove /tcp or /udp from port_external if it exists
if (port_external.includes("/")) {
port_external = port_external.split("/")[0];
}
ports_data.push({
check: port_check,
external: port_external,
internal: port_internal,
protocol: port_protocol
});
} catch {
ports_data.push({
check: "",
external: "",
internal: "",
protocol: ""
});
}
// Get volume details
try {
let volumes = result.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
volumes_data.push({
check: volume_check,
bind: volume_bind,
container: volume_container,
readwrite: volume_readwrite
});
} catch {
volumes_data.push({
check: "",
bind: "",
container: "",
readwrite: ""
});
}
// Get environment details
try {
let env = result.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";
env_data.push({
check: env_check,
default: env_default,
description: env_description,
label: env_label,
name: env_name
});
} catch {
env_data.push({
check: "",
default: "",
description: "",
label: "",
name: ""
});
}
// Get label details
try {
let label = result.labels[i];
let label_check = "";
let label_name = label.name ? label.name : "";
let label_value = label.value ? label.value : "";
label_data.push({
check: label_check,
name: label_name,
value: label_value
});
} catch {
label_data.push({
check: "",
name: "",
value: ""
});
}
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
let modal = readFileSync('./views/modals/json.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppNote/g, note);
modal = modal.replace(/AppImage/g, image);
modal = modal.replace(/RestartPolicy/g, restart_policy);
modal = modal.replace(/NetHost/g, net_host);
modal = modal.replace(/NetBridge/g, net_bridge);
modal = modal.replace(/NetDocker/g, net_docker);
modal = modal.replace(/NetName/g, net_name);
modal = modal.replace(/ModalName/g, modal_name);
modal = modal.replace(/FormId/g, form_id);
modal = modal.replace(/CommandCheck/g, command_check);
modal = modal.replace(/CommandValue/g, command);
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
for (let i = 0; i < 12; i++) {
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
}
res.send(modal);
}
}
export const LearnMore = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
if (id == 'compose') {
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, name);
modal = modal.replace(/AppDesc/g, 'Compose File');
res.send(modal);
return;
}
let result = templates_global.find(t => t.name == name);
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
modal = modal.replace(/AppName/g, result.title);
modal = modal.replace(/AppDesc/g, result.description);
res.send(modal);
}
export const ImportModal = async (req, res) => {
let modal = readFileSync('./views/modals/import.html', 'utf8');
res.send(modal);
}
export const Upload = (req, res) => {
upload.array('files', 10)(req, res, () => {
alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" 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="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template(s) Uploaded!</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
<div class="d-flex">
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" 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="M5 12l5 5l10 -10"></path></svg> </div>
<div>Template already exists</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
</div>`;
let files = readdirSync('templates/tmp/');
for (let i = 0; i < files.length; i++) {
if (files[i].endsWith('.zip')) {
let zip = new AdmZip(`templates/tmp/${files[i]}`);
zip.extractAllTo('templates/compose', true);
unlinkSync(`templates/tmp/${files[i]}`);
} else if (files[i].endsWith('.json')) {
if (existsSync(`templates/json/${files[i]}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
} else if (files[i].endsWith('.yml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else if (files[i].endsWith('.yaml')) {
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
let compose_data = parse(compose);
let service_name = Object.keys(compose_data.services);
if (existsSync(`templates/compose/${service_name}`)) {
unlinkSync(`templates/tmp/${files[i]}`);
alert = exists_alert;
res.redirect('/apps');
return;
}
mkdirSync(`templates/compose/${service_name}`);
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
} else {
// unsupported file type
unlinkSync(`templates/tmp/${files[i]}`);
}
}
res.redirect('/apps');
});
};

View file

@ -1,150 +0,0 @@
const User = require('../database/UserModel');
const bcrypt = require('bcrypt');
exports.Login = function(req,res){
// check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
}else{
// Render the login page.
res.render("pages/login",{
"error":"",
"isLoggedIn": false
});
}
}
exports.processLogin = async function(req,res){
// get the data.
let email = req.body.email;
let password = req.body.password;
// check if we have data.
if(email && password){
// check if the user exists.
let existingUser = await User.findOne({ where: {email:email}});
if(existingUser){
// compare the password.
let match = await bcrypt.compare(password,existingUser.password);
if(match){
// set the session.
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
// Redirect to the home page.
res.redirect("/");
}else{
// return an error.
res.render("pages/login",{
"error":"Invalid password",
isLoggedIn: false
});
}
}else{
// return an error.
res.render("pages/login",{
"error":"User with that email does not exist.",
isLoggedIn:false
});
}
}else{
res.status(400);
res.render("pages/login",{
"error":"Please fill in all the fields.",
isLoggedIn:false
});
}
}
exports.Logout = function(req,res){
// clear the session.
req.session.destroy();
// Redirect to the login page.
res.redirect("/login");
}
exports.Register = function(req,res){
// Check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
} else {
// Render the signup page.
res.render("pages/register",{
"error":"",
isLoggedIn:false
});
}
}
exports.processRegister = async function(req,res){
// Get the data.
let { first_name, last_name, username, email, password, avatar, tos } = req.body;
let role = "user";
// Check the data.
if(first_name && last_name && email && password && username && tos){
// Check if there is an existing user with that username.
let existingUser = await User.findOne({ where: {username:username}});
let adminUser = await User.findOne({ where: {role:"admin"}});
if(!existingUser){
// hash the password.
let hashedPassword = bcrypt.hashSync(password,10);
if(!adminUser){
console.log('Creating admin User');
role = "admin";
}
try {
const user = await User.create({
first_name: first_name,
last_name: last_name,
username: username,
email: email,
password: hashedPassword,
role: role,
group: 'all',
avatar: `<img src="./static/avatars/${avatar}">`
});
// set the session.
req.session.user = user.username;
req.session.UUID = user.UUID;
req.session.role = user.role;
// Redirect to the home page.
res.redirect("/");
}
catch (err) {
// return an error.
res.render("pages/register",{
"error":"Something went wrong when creating account.",
isLoggedIn:false
});
}
}else{
// return an error.
res.render("pages/register",{
"error":"User with that username already exists.",
isLoggedIn:false
});
}
}else{
// Redirect to the signup page.
res.render("pages/register",{
"error":"Please fill in all the fields and accept TOS.",
isLoggedIn:false
});
}
}

View file

@ -1,247 +1,448 @@
const User = require('../database/UserModel');
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
const { execSync } = require("child_process");
const { siteCard } = require('../components/siteCard');
const { containerExec } = require('../functions/system')
import { Readable } from 'stream';
import { Permission, User } from '../database/models.js';
import { docker } from '../server.js';
import { dockerContainerStats } from 'systeminformation';
import { readFileSync } from 'fs';
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
import { Op } from 'sequelize';
let hidden = '';
let alert = '';
let [ cardList, newCards, stats ] = [ '', '', {}];
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
// The page
export const Dashboard = (req, res) => {
exports.Dashboard = async function (req, res) {
if (req.session.role == "admin") {
// get user data with matching UUID from sqlite database
let user = await User.findOne({ where: { UUID: req.session.UUID } });
let caddy = 'd-none';
if (process.env.Proxy_Manager == 'enabled') {
caddy = '';
}
// Render the home page
res.render("pages/dashboard", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
site_list: req.app.locals.site_list,
caddy: caddy
});
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.AddSite = async function (req, res) {
let { domain, type, host, port } = req.body;
if ((req.session.role == "admin") && ( domain && type && host && port)) {
let { domain, type, host, port } = req.body;
// build caddyfile
let caddyfile = `${domain} {`
caddyfile += `\n\t${type} ${host}:${port}`
caddyfile += `\n\theader {`
caddyfile += `\n\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains; preload"`
caddyfile += `\n\t}`
caddyfile += `\n}`
// save caddyfile
writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
// format caddyfile
let format = {
container: 'DweebProxy',
command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
}
await containerExec(format, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Formatted ${domain}.Caddyfile`);
});
///////////////// convert caddyfile to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
}
await containerExec(convert, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Converted ${domain}.Caddyfile to JSON`);
});
////////////// reload caddy
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Reloaded Caddy Config`);
});
let site = siteCard(type, domain, host, port, 0);
req.app.locals.site_list += site;
res.redirect("/");
} else {
// Redirect
console.log('not admin or missing info')
res.redirect("/");
}
}
exports.RemoveSite = async function (req, res) {
if (req.session.role == "admin") {
for (const [key, value] of Object.entries(req.body)) {
execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${value}.Caddyfile`);
});
}
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload);
console.log('Removed Site(s)')
res.redirect("/refreshsites");
} else {
res.redirect("/");
}
let name = req.session.user;
let role = req.session.role;
alert = req.session.alert;
res.render("dashboard", {
name: name,
avatar: name.charAt(0).toUpperCase(),
role: role,
alert: alert,
});
}
// The page actions
export const DashboardAction = async (req, res) => {
let name = req.header('hx-trigger-name');
let value = req.header('hx-trigger');
let action = req.params.action;
let modal = '';
exports.RefreshSites = async function (req, res) {
let domain, type, host, port;
let id = 1;
if (req.session.role == "admin") {
// Clear site_list.ejs
req.app.locals.site_list = "";
// check if ./caddyfiles/sites contains any .json files, then delete them
try {
let files = readdirSync('./caddyfiles/sites/');
files.forEach(file => {
if (file.includes(".json")) {
execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${file}`);
});
}
});
} catch (error) { console.log("No .json files to delete") }
// get list of Caddyfiles
let sites = readdirSync('./caddyfiles/sites/');
sites.forEach(site_name => {
// convert the caddyfile of each site to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
switch (action) {
case 'permissions':
let title = name.charAt(0).toUpperCase() + name.slice(1);
let permissions_list = '';
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
let users = await User.findAll({ attributes: ['username', 'UUID']});
for (let i = 0; i < users.length; i++) {
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
permissions_list += user_permissions;
}
containerExec(convert);
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
res.send(permissions_modal);
return;
case 'uninstall':
modal = readFileSync('./views/modals/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, name);
res.send(modal);
return;
case 'details':
modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
try {
// read the json file
let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
// fix whitespace and parse the json file
site_file = site_file.replace(/ /g, " ");
site_file = JSON.parse(site_file);
} catch (error) { console.log("No .json file to read") }
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
}
for (let i = 0; i <= 6; i++) {
modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
}
// get the domain, type, host, and port from the json file
try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
try { type = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].handler } catch (error) { console.log("No Type") }
try { host = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[0] } catch (error) { console.log("Not Localhost") }
try { port = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[1] } catch (error) { console.log("No Port") }
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
}
// build the site card
let site = siteCard(type, domain, host, port, id);
// console.log(details.env);
for (let i = 0; i <= 19; i++) {
modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
}
// append the site card to site_list
req.app.locals.site_list += site;
id++;
});
res.send(modal);
return;
case 'updates':
res.send(newCards);
newCards = '';
return;
case 'card':
await userCards(req.session);
if (!req.session.container_list.find(c => c.container === name)) {
res.send('');
return;
} else {
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
return;
}
case 'logs':
let logString = '';
let options = { follow: true, stdout: true, stderr: false, timestamps: false };
docker.getContainer(name).logs(options, function (err, stream) {
if (err) { console.log(err); return; }
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
res.send(`<pre>${logString}</pre>`);
});
});
return;
case 'hide':
let user = req.session.user;
let exists = await Permission.findOne({ where: {containerName: name, user: user}});
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
else { exists.update({ hide: true }); }
hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
res.send("ok");
return;
case 'reset':
await Permission.update({ hide: false }, { where: { user: req.session.user } });
res.send("ok");
return;
case 'alert':
req.session.alert = '';
res.send('');
return;
}
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/");
function status (state) {
return(`<span class="text-yellow align-items-center lh-1"><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>
${state}
</span>`);
}
// Container actions
if ((action == 'start') && (value == 'stopped')) {
docker.getContainer(name).start();
res.send(status('starting'));
} else if ((action == 'start') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'stop') && (value != 'stopped')) {
docker.getContainer(name).stop();
res.send(status('stopping'));
} else if ((action == 'pause') && (value == 'paused')) {
docker.getContainer(name).unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (value == 'running')) {
docker.getContainer(name).pause();
res.send(status('pausing'));
} else if (action == 'restart') {
docker.getContainer(name).restart();
res.send(status('restarting'));
}
}
async function containerInfo (containerName) {
// get the container info
let container = docker.getContainer(containerName);
let info = await container.inspect();
let image = info.Config.Image;
// grab the service name from the end of the image name
let service = image.split('/').pop();
// remove the tag from the service name if it exists
try { service = service.split(':')[0]; } catch {}
let ports_list = [];
let external = 0;
let internal = 0;
try {
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {}
try {
external = ports_list[0].external;
internal = ports_list[0].internal;
} catch {}
// console.log(ports_list);
// console.log(info.HostConfig.PortBindings);
// console.log(info.HostConfig.Binds);
// console.log(info.Config.Env);
// console.log(info.Config.Labels);
let details = {
name: containerName,
image: image,
service: service,
state: info.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
volumes: info.Mounts,
env: info.Config.Env,
labels: info.Config.Labels,
link: 'localhost',
}
return details;
}
async function createCard (details) {
let shortname = details.name.slice(0, 10) + '...';
let trigger = 'data-hx-trigger="load, every 3s"';
let state = details.state;
let card = readFileSync('./views/partials/containerFull.html', 'utf8');
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state = 'stopped';
state_color = 'red';
trigger = 'data-hx-trigger="load"';
break;
case 'paused':
state_color = 'orange';
trigger = 'data-hx-trigger="load"';
break;
case 'installing':
state_color = 'blue';
trigger = 'data-hx-trigger="load"';
break;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
card = card.replace(/AppName/g, details.name);
card = card.replace(/AppShortName/g, shortname);
card = card.replace(/AppIcon/g, details.service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ExternalPort/g, details.external_port);
card = card.replace(/InternalPort/g, details.internal_port);
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
async function userCards (session) {
session.container_list = [];
// check what containers the user wants hidden
let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
hidden = hidden.map((container) => container.containerName);
// check what containers the user has permission to view
let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
visable = visable.map((container) => container.containerName);
// get all containers
let containers = await docker.listContainers({ all: true });
// loop through containers
for (let i = 0; i < containers.length; i++) {
let container_name = containers[i].Names[0].replace('/', '');
// skip hidden containers
if (hidden.includes(container_name)) { continue; }
// admin can see all containers that they don't have hidden
if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
// user can see any containers that they have any permissions for
else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
}
// create a sent list if it doesn't exist
if (!session.sent_list) { session.sent_list = []; }
if (!session.update_list) { session.update_list = []; }
if (!session.new_cards) { session.new_cards = []; }
}
async function updateDashboard (session) {
let container_list = session.container_list;
let sent_list = session.sent_list;
session.new_cards = [];
session.update_list = [];
// loop through the containers list
container_list.forEach(info => {
let { container, state } = info;
let sent = sent_list.find(c => c.container === container);
if (!sent) { session.new_cards.push(container);}
else if (sent.state !== state) { session.update_list.push(container); }
});
// loop through the sent list to see if any containers have been removed
sent_list.forEach(info => {
let { container } = info;
let exists = container_list.find(c => c.container === container);
if (!exists) { session.update_list.push(container); }
});
}
// HTMX server-side events
export const SSE = async (req, res) => {
// set the headers for server-sent events
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
// check for container changes every 500ms
let eventCheck = setInterval(async () => {
await userCards(req.session);
// check if the cards displayed are the same as what's in the session
if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
await updateDashboard(req.session);
for (let i = 0; i < req.session.new_cards.length; i++) {
let details = await containerInfo(req.session.new_cards[i]);
let card = await createCard(details);
newCards += card;
req.session.alert = '';
}
for (let i = 0; i < req.session.update_list.length; i++) {
res.write(`event: ${req.session.update_list[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
res.write(`event: update\n`);
res.write(`data: 'update cards'\n\n`);
req.session.sent_list = req.session.container_list.slice();
}, 500);
req.on('close', () => {
clearInterval(eventCheck);
});
};
// Server metrics (CPU, RAM, TX, RX, DISK)
export const Stats = async (req, res) => {
let name = req.header('hx-trigger-name');
let color = req.header('hx-trigger');
let value = 0;
switch (name) {
case 'CPU':
await currentLoad().then(data => { value = Math.round(data.currentLoad); });
break;
case 'RAM':
await mem().then(data => { value = Math.round((data.active / data.total) * 100); });
break;
case 'NET':
let [down, up, percent] = [0, 0, 0];
await networkStats().then(data => { down = Math.round(data[0].rx_bytes / (1024 * 1024)); up = Math.round(data[0].tx_bytes / (1024 * 1024)); percent = Math.round((down / 1000) * 100); });
let net = `<div class="font-weight-medium"><label class="cpu-text mb-1">Down:${down}MB Up:${up}MB</label></div>
<div class="cpu-bar meter animate ${color}"><span style="width:20%"><span></span></span></div>`;
res.send(net);
return;
case 'DISK':
await fsSize().then(data => { value = data[0].use; });
break;
}
let info = `<div class="font-weight-medium"> <label class="cpu-text mb-1">${name} ${value}%</label></div>
<div class="cpu-bar meter animate ${color}"> <span style="width:${value}%"><span></span></span> </div>`;
res.send(info);
}
// Imported by utils/install.js
export async function addAlert (session, type, message) {
session.alert = `<div class="alert alert-${type} alert-dismissible py-2 mb-0" role="alert" id="alert">
<div class="d-flex">
<div class="spinner-border text-info nav-link">
<span class="visually-hidden">Loading...</span>
</div>
<div>
  ${message}
</div>
</div>
<button class="btn-close" data-hx-post="/dashboard/alert" data-hx-trigger="click" data-hx-target="#alert" data-hx-swap="outerHTML" style="padding-top: 0.5rem;" ></button>
</div>`;
}
export const UpdatePermissions = async (req, res) => {
let { user, container, reset_permissions } = req.body;
let id = req.header('hx-trigger');
if (reset_permissions) {
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerName: container} });
return;
}
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false }, { where: { containerName: container, user: user } });
Object.keys(req.body).forEach(async function(key) {
if (key != 'user' && key != 'container') {
let permissions = req.body[key];
if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('start')) { await Permission.update({ start: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: {containerName: container, user: user}}); }
if (permissions.includes('view')) { await Permission.update({ view: true }, { where: {containerName: container, user: user}}); }
}
});
if (id == 'submit') {
res.send('<button class="btn" type="button" id="confirmed" hx-post="/updatePermissions" hx-swap="outerHTML" hx-trigger="load delay:2s">Update ✔️</button>');
return;
} else if (id == 'confirmed') {
res.send('<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions" hx-swap="outerHTML">Update </button>');
return;
}
}
exports.DisableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Disable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.EnableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Enable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
// Container charts
export const Chart = async (req, res) => {
let name = req.header('hx-trigger-name');
if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
const info = await dockerContainerStats(name);
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
stats[name].ramArray.push(Math.round(info[0].memPercent));
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
stats[name].ramArray = stats[name].ramArray.slice(-15);
let chart = `
<script>
${name}chart.updateSeries([{
data: [${stats[name].cpuArray}]
}, {
data: [${stats[name].ramArray}]
}])
</script>`
res.send(chart);
}

110
controllers/images.js Normal file
View file

@ -0,0 +1,110 @@
import { docker } from '../server.js';
import { addAlert } from './dashboard.js';
export const Images = async function(req, res) {
let action = req.params.action;
if (action == "remove") {
let images = req.body.select;
if (typeof(images) == 'string') {
images = [images];
}
for (let i = 0; i < images.length; i++) {
if (images[i] != 'on') {
try {
console.log(`Removing image: ${images[i]}`);
let image = docker.getImage(images[i]);
await image.remove();
} catch (error) {
console.log(`Unable to remove image: ${images[i]}`);
}
}
}
res.redirect("/images");
return;
} else if (action == "add") {
let image = req.body.image;
let tag = req.body.tag || 'latest';
try {
console.log(`Pulling image: ${image}:${tag}`);
await docker.pull(`${image}:${tag}`);
} catch (error) {
console.log(`Unable to pull image: ${image}:${tag}`);
}
res.redirect("/images");
return;
}
let containers = await docker.listContainers({ all: true });
let container_images = [];
for (let i = 0; i < containers.length; i++) {
container_images.push(containers[i].Image);
}
let images = await docker.listImages({ all: true });
let image_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-type">Tag</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < images.length; i++) {
let name = '';
let tag = '';
try { name = images[i].RepoTags[0].split(':')[0]; } catch {}
try { tag = images[i].RepoTags[0].split(':')[1]; } catch {}
let date = new Date(images[i].Created * 1000);
let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let size = images[i].Size / 1000 / 1000; // to match docker desktop
size = size.toFixed(2);
let status = '';
if (container_images.includes(images[i].RepoTags[0])) {
status = 'In use';
}
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${name}</td>
<td class="sort-type">${tag}</td>
<td class="sort-city">${images[i].Id}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${created}</td>
<td class="sort-quantity">${size} MB</td>
<td class="text-end"><a class="btn" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
</tr>`
image_list += details;
}
image_list += `</tbody>`
res.render("images", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
image_list: image_list,
image_count: images.length,
alert: '',
});
}

71
controllers/login.js Normal file
View file

@ -0,0 +1,71 @@
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
export const Login = function(req,res){
if (req.session.user) { res.redirect("/logout"); }
else { res.render("login",{ "error":"", }); }
}
export const submitLogin = async function(req,res){
let { email, password } = req.body;
email = email.toLowerCase();
if (email && password) {
let existingUser = await User.findOne({ where: {email:email}});
if (existingUser) {
let match = await bcrypt.compare(password,existingUser.password);
if (match) {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
req.session.avatar = existingUser.avatar;
const syslog = await Syslog.create({
user: req.session.user,
email: email,
event: "Successful Login",
message: "User logged in successfully",
ip: req.socket.remoteAddress
});
res.redirect("/dashboard");
} else {
const syslog = await Syslog.create({
user: null,
email: email,
event: "Bad Login",
message: "Invalid password",
ip: req.socket.remoteAddress
});
res.render("login",{
"error":"Invalid password",
});
}
} else {
res.render("login",{
"error":"User with that email does not exist.",
});
}
} else {
res.status(400);
res.render("login",{
"error":"Please fill in all the fields.",
});
}
}
export const Logout = function(req,res){
req.session.destroy(() => {
res.redirect("/login");
});
}

89
controllers/networks.js Normal file
View file

@ -0,0 +1,89 @@
import { docker } from '../server.js';
export const Networks = async function(req, res) {
let container_networks = [];
// List all containers
let containers = await docker.listContainers({ all: true });
for (let i = 0; i < containers.length; i++) {
let network_name = containers[i].HostConfig.NetworkMode;
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID) } catch {}
}
let networks = await docker.listNetworks({ all: true });
let network_list = `
<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
for (let i = 0; i < networks.length; i++) {
// let date = new Date(images[i].Created * 1000);
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
let status = '';
if (container_networks.includes(networks[i].Id)) {
status = `In use`;
}
let details = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
<td class="sort-name">${networks[i].Name}</td>
<td class="sort-city">${networks[i].Id}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
network_list += details;
}
network_list += `</tbody>`
res.render("networks", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
network_list: network_list,
network_count: networks.length,
alert: '',
});
}
export const removeNetwork = async function(req, res) {
let networks = req.body.select;
if (typeof(networks) == 'string') {
networks = [networks];
}
for (let i = 0; i < networks.length; i++) {
if (networks[i] != 'on') {
try {
console.log(`Removing network: ${networks[i]}`);
let network = docker.getNetwork(networks[i]);
await network.remove();
} catch (error) {
console.log(`Unable to remove network: ${networks[i]}`);
}
}
}
res.redirect("/networks");
}

389
controllers/portal.js Normal file
View file

@ -0,0 +1,389 @@
import { Readable } from 'stream';
import { Permission, Container, User } from '../database/models.js';
import { docker } from '../server.js';
import { readFileSync } from 'fs';
let hidden = '';
// The actual page
export const Portal = (req, res) => {
let name = req.session.user;
let role = req.session.role;
let avatar = name.charAt(0).toUpperCase();
res.render("portal", {
name: name,
avatar: avatar,
role: role,
alert: '',
});
}
async function CardList () {
let name = req.session.user;
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
for (let i = 0; i < containers.length; i++) {
let details = await containerInfo(containers[i].containerName);
let card = await createCard(details);
cardList += card;
}
}
export const UserContainers = async (req, res) => {
let cardList = '';
let name = req.session.user;
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
for (let i = 0; i < containers.length; i++) {
if (containers[i].containerName == null) { continue; }
let details = await containerInfo(containers[i].containerName);
let card = await createCard(details);
cardList += card;
}
res.send(cardList);
}
async function containerInfo (containerName) {
let container = docker.getContainer(containerName);
let info = await container.inspect();
let image = info.Config.Image.split('/');
let ports_list = [];
try {
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
let ports = {
check: 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch {
// no exposed ports
}
let external = 0;
let internal = 0;
try {
external = ports_list[0].external;
internal = ports_list[0].internal;
} catch {
// no exposed ports
}
let details = {
name: containerName,
image: image,
service: image[image.length - 1].split(':')[0],
state: info.State.Status,
external_port: external,
internal_port: internal,
ports: ports_list,
link: 'localhost',
}
return details;
}
async function createCard (details) {
if (hidden.includes(details.name)) { return;}
let shortname = details.name.slice(0, 10) + '...';
let trigger = 'data-hx-trigger="load, every 3s"';
let state = details.state;
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state = 'stopped';
state_color = 'red';
trigger = 'data-hx-trigger="load"';
break;
case 'paused':
state_color = 'orange';
trigger = 'data-hx-trigger="load"';
break;
case 'installing':
state_color = 'blue';
trigger = 'data-hx-trigger="load"';
break;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
card = card.replace(/AppName/g, details.name);
card = card.replace(/AppShortName/g, shortname);
card = card.replace(/AppIcon/g, details.service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ExternalPort/g, details.external_port);
card = card.replace(/InternalPort/g, details.internal_port);
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
export async function addCard (name, state) {
console.log(`Adding card for ${name}: ${state}`);
let details = {
name: name,
image: name,
service: name,
state: 'installing',
external_port: 0,
internal_port: 0,
ports: [],
link: 'localhost',
}
createCard(details).then(card => {
cardList += card;
});
}
// HTMX server-side events
export const SSE = async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
let eventCheck = setInterval(async () => {
// builds array of containers and their states
containersArray = [];
await docker.listContainers({ all: true }).then(containers => {
containers.forEach(container => {
let name = container.Names[0].replace('/', '');
if (!hidden.includes(name)) { // if not hidden
containersArray.push({ container: name, state: container.State });
}
});
});
if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
cardList = '';
newCards = '';
containersArray.forEach(container => {
const { container: containerName, state } = container;
const existingContainer = sentArray.find(c => c.container === containerName);
if (!existingContainer) {
containerInfo(containerName).then(details => {
createCard(details).then(card => {
newCards += card;
});
});
res.write(`event: update\n`);
res.write(`data: 'update cards'\n\n`);
} else if (existingContainer.state !== state) {
updatesArray.push(containerName);
}
containerInfo(containerName).then(details => {
createCard(details).then(card => {
cardList += card;
});
});
});
sentArray.forEach(container => {
const { container: containerName } = container;
const existingContainer = containersArray.find(c => c.container === containerName);
if (!existingContainer) {
updatesArray.push(containerName);
}
});
for (let i = 0; i < updatesArray.length; i++) {
res.write(`event: ${updatesArray[i]}\n`);
res.write(`data: 'update cards'\n\n`);
}
updatesArray = [];
sentArray = containersArray.slice();
}
}, 500);
req.on('close', () => {
clearInterval(eventCheck);
});
};
export const updateCards = async (req, res) => {
console.log('updateCards called');
res.send(newCards);
newCards = '';
}
export const Containers = async (req, res) => {
CardList();
// res.send(cardList);
}
export const Card = async (req, res) => {
let name = req.header('hx-trigger-name');
console.log(`${name} requesting updated card`);
// return nothing if in hidden or not found in containersArray
if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
res.send('');
return;
} else {
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
}
}
function status (state) {
let status = `<span class="text-yellow align-items-center lh-1">
<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>
${state}
</span>`;
return status;
}
export const Logs = (req, res) => {
let name = req.header('hx-trigger-name');
function containerLogs (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = { follow: false, stdout: true, stderr: false, timestamps: false };
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) { reject(err); return; }
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};
containerLogs(name).then((data) => {
res.send(`<pre>${data}</pre> `)
});
}
export const Action = async (req, res) => {
let name = req.header('hx-trigger-name');
let state = req.header('hx-trigger');
let action = req.params.action;
// Start
if ((action == 'start') && (state == 'stopped')) {
var containerName = docker.getContainer(name);
containerName.start();
res.send(status('starting'));
} else if ((action == 'start') && (state == 'paused')) {
var containerName = docker.getContainer(name);
containerName.unpause();
res.send(status('starting'));
// Stop
} else if ((action == 'stop') && (state != 'stopped')) {
var containerName = docker.getContainer(name);
containerName.stop();
res.send(status('stopping'));
// Pause
} else if ((action == 'pause') && (state == 'paused')) {
var containerName = docker.getContainer(name);
containerName.unpause();
res.send(status('starting'));
} else if ((action == 'pause') && (state == 'running')) {
var containerName = docker.getContainer(name);
containerName.pause();
res.send(status('pausing'));
// Restart
} else if (action == 'restart') {
var containerName = docker.getContainer(name);
containerName.restart();
res.send(status('restarting'));
// Hide
} else if (action == 'hide') {
let exists = await Container.findOne({ where: {name: name}});
if (!exists) {
const newContainer = await Container.create({ name: name, visibility: false, });
} else {
exists.update({ visibility: false });
}
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
res.send("ok");
// Reset View
} else if (action == 'reset') {
await Container.update({ visibility: true }, { where: {} });
hidden = await Container.findAll({ where: {visibility:false}});
hidden = hidden.map((container) => container.name);
res.send("ok");
}
}
export const Modals = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
let title = name.charAt(0).toUpperCase() + name.slice(1);
if (id == 'permissions') {
let permissions_list = '';
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
let users = await User.findAll({ attributes: ['username', 'UUID']});
for (let i = 0; i < users.length; i++) {
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (!exists) {
const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
}
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
user_permissions = user_permissions.replace(/EntryNumber/g, i);
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
permissions_list += user_permissions;
}
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
res.send(permissions_modal);
return;
}
if (id == 'uninstall') {
let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
modal = modal.replace(/AppName/g, name);
// let containerPermissions = await Permission.findAll({ where: {containerName: name}});
res.send(modal);
return;
}
let modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
res.send(modal);
}

104
controllers/register.js Normal file
View file

@ -0,0 +1,104 @@
import { User, Syslog, Permission } from '../database/models.js';
import bcrypt from 'bcrypt';
let SECRET = process.env.SECRET || "MrWiskers"
export const Register = function(req,res){
if(req.session.user){
res.redirect("/logout");
} else {
res.render("register",{
"error":"",
});
}
}
export const submitRegister = async function(req,res){
let { name, username, email, password, confirmPassword, secret } = req.body;
email = email.toLowerCase();
if (secret != SECRET) {
const syslog = await Syslog.create({
user: username,
email: email,
event: "Failed Registration",
message: "Invalid secret",
ip: req.socket.remoteAddress
});
}
if((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
async function userRole () {
let userCount = await User.count();
if(userCount == 0){
return "admin";
}else{
return "user";
}
}
let existingUser = await User.findOne({ where: {email:email}});
if(!existingUser){
try {
let currentDate = new Date();
let newLogin = currentDate.toLocaleString();
const user = await User.create({
name: name,
username: username,
email: email,
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
lastLogin: newLogin,
});
// make sure the user was created and get the UUID.
let newUser = await User.findOne({ where: {email:email}});
let match = await bcrypt.compare(password,newUser.password);
if(match){
req.session.user = newUser.username;
req.session.UUID = newUser.UUID;
req.session.role = newUser.role;
const permission = await Permission.create({
user: newUser.username,
userID: newUser.UUID
});
const syslog = await Syslog.create({
user: req.session.user,
email: email,
event: "Successful Registration",
message: "User registered successfully",
ip: req.socket.remoteAddress
});
res.redirect("/dashboard");
}
} catch(err) {
res.render("register",{
"error":"Something went wrong when creating account.",
});
}
} else {
// return an error.
res.render("register",{
"error":"User with that email already exists.",
});
}
} else {
// Redirect to the signup page.
res.render("register",{
"error":"Please fill in all the fields.",
});
}
}

View file

@ -1,22 +1,10 @@
const User = require('../database/UserModel.js');
const Server = require('../database/ServerSettings.js');
exports.Settings = async function(req, res) {
if (req.session.role == "admin") {
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
export const Settings = (req, res) => {
// Render the home page
res.render("pages/settings", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true
});
} else {
// Redirect to the login page
res.redirect("/login");
}
res.render("settings", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
}

31
controllers/supporters.js Normal file
View file

@ -0,0 +1,31 @@
import { User } from "../database/models.js";
export const Supporters = async (req, res) => {
let user = await User.findOne({ where: { UUID: req.session.UUID }});
res.render("supporters", {
first_name: user.name,
last_name: user.name,
name: user.name,
id: user.id,
email: user.email,
role: user.role,
avatar: req.session.user.charAt(0).toUpperCase(),
alert: '',
});
}
let thanks = 0;
export const Thanks = async (req, res) => {
thanks++;
let data = thanks.toString();
if (thanks > 999) {
data = 'Did you really click 1000 times?!';
}
res.send(data);
}

37
controllers/syslogs.js Normal file
View file

@ -0,0 +1,37 @@
import { Syslog } from '../database/models.js';
export const Syslogs = async function(req, res) {
let logs = '';
const syslogs = await Syslog.findAll({
order: [
['id', 'DESC']
]
});
for (const log of syslogs) {
let date = (log.createdAt).toDateString();
let time = (log.createdAt).toLocaleTimeString();
let datetime = `${time} ${date}`;
logs += `<tr>
<td class="sort-id">${log.id}</td>
<td class="sort-user">${log.user}</td>
<td class="sort-email">${log.email}</td>
<td class="sort-event">${log.event}</td>
<td class="sort-message">${log.message}</td>
<td class="sort-ip">${log.ip}</td>
<td class="sort-datetime">${datetime}</td>
</tr>`
}
res.render("syslogs", {
name: req.session.user || 'Dev',
role: req.session.role || 'Dev',
avatar: req.session.user.charAt(0).toUpperCase(),
logs: logs,
alert: '',
});
}

View file

@ -1,54 +1,62 @@
const User = require('../database/UserModel');
import { User } from '../database/models.js';
exports.Users = async function(req, res) {
if (req.session.role == "admin") {
export const Users = async (req, res) => {
let user_list = `
<tr>
<th><input class="form-check-input" type="checkbox"></th>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Last Login</th>
<th>Status</th>
<th>Actions</th>
</tr>`
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let user_list = `
let allUsers = await User.findAll();
allUsers.forEach((account) => {
let active = '<span class="badge badge-outline text-green" title="User has logged-in within the last 30 days.">Active</span>'
let lastLogin = new Date(account.lastLogin);
let currentDate = new Date();
let days = Math.floor((currentDate - lastLogin) / (1000 * 60 * 60 * 24));
let avatar = account.username.charAt(0);
if (days > 30) {
active = '<span class="badge badge-outline text-grey" title="User has not logged-in within the last 30 days.">Inactive</span>';
}
let info = `
<tr>
<th><input class="form-check-input" type="checkbox"></th>
<th>ID</th>
<th>Avatar</th>
<th>Name</th>
<th>Username</th>
<th>Email</th>
<th>UUID</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
<td><input class="form-check-input" type="checkbox"></td>
<td>${account.id}</td>
<td><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
<td>${account.name}</td>
<td>${account.username}</td>
<td>${account.email}</td>
<td>${account.UUID}</td>
<td>${account.role}</td>
<td>${account.lastLogin}</td>
<td>${active}</td>
<td><a href="#" class="btn">View</a></td>
</tr>`
let users = await User.findAll();
users.forEach((account) => {
full_name = account.first_name + ' ' + account.last_name;
user_info = `
<tr>
<td><input class="form-check-input" type="checkbox"></td>
<td>${user.id}</td>
<td><span class="avatar me-2">${account.avatar}</span></td>
<td>${full_name}</td>
<td>${account.username}</td>
<td>${account.email}</td>
<td>${account.UUID}</td>
<td>${account.role}</td>
<td><span class="badge badge-outline text-green">Active</span></td>
<td><a href="#" class="btn">Edit</a></td>
</tr>`
user_list += info;
});
user_list += user_info;
});
// Render the home page
res.render("pages/users", {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true,
user_list: user_list
});
} else {
// Redirect to the login page
res.redirect("/login");
}
res.render("users", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
user_list: user_list,
alert: ''
});
}

9
controllers/variables.js Normal file
View file

@ -0,0 +1,9 @@
export const Variables = (req, res) => {
res.render("variables", {
name: req.session.user,
role: req.session.role,
avatar: req.session.avatar,
});
}

119
controllers/volumes.js Normal file
View file

@ -0,0 +1,119 @@
import { docker } from '../server.js';
export const Volumes = async function(req, res) {
let container_volumes = [];
let volume_list = '';
// Table header
volume_list = `<thead>
<tr>
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
<th><label class="table-sort" data-sort="sort-type">Type</label></th>
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
<th><label class="table-sort" data-sort="sort-city">Mount point</label></th>
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
</tr>
</thead>
<tbody class="table-tbody">`
// List all containers
let containers = await docker.listContainers({ all: true });
// Get the first 6 volumes from each container
for (let i = 0; i < containers.length; i++) {
try { container_volumes.push({type: containers[i].Mounts[0].Type, source: containers[i].Mounts[0].Source}); } catch { }
try { container_volumes.push({type: containers[i].Mounts[1].Type, source: containers[i].Mounts[1].Source}); } catch { }
try { container_volumes.push({type: containers[i].Mounts[2].Type, source: containers[i].Mounts[2].Source}); } catch { }
try { container_volumes.push({type: containers[i].Mounts[3].Type, source: containers[i].Mounts[3].Source}); } catch { }
try { container_volumes.push({type: containers[i].Mounts[4].Type, source: containers[i].Mounts[4].Source}); } catch { }
try { container_volumes.push({type: containers[i].Mounts[5].Type, source: containers[i].Mounts[5].Source}); } catch { }
}
// List ALL volumes
let list = await docker.listVolumes({ all: true });
let volumes = list.Volumes;
// Create a table row for each volume
for (let i = 0; i < volumes.length; i++) {
let volume = volumes[i];
let name = "" + volume.Name;
let mount = "" + volume.Mountpoint;
let type = "Bind";
// Check if the volume is being used by any of the containers
let status = '';
if (container_volumes.some(volume => volume.source === mount)) { status = "In use"; }
if (container_volumes.some(volume => volume.source === mount && volume.type === 'volume')) { type = "Volume"; }
let row = `
<tr>
<td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
<td class="sort-type">${type}</td>
<td class="sort-name">${name}</td>
<td class="sort-city">${mount}</td>
<td class="sort-score text-green">${status}</td>
<td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
<td class="sort-quantity">MB</td>
<td class="text-end"><a class="btn" href="#">Details</a></td>
</tr>`
volume_list += row;
}
volume_list += `</tbody>`
res.render("volumes", {
name: req.session.user,
role: req.session.role,
avatar: req.session.user.charAt(0).toUpperCase(),
volume_list: volume_list,
volume_count: volumes.length,
alert: '',
});
}
export const addVolume = async function(req, res) {
let volume = req.body.volume;
docker.createVolume({
Name: volume
});
res.redirect("/volumes");
}
export const removeVolume = async function(req, res) {
let volumes = req.body.select;
if (typeof(volumes) == 'string') {
volumes = [volumes];
}
for (let i = 0; i < volumes.length; i++) {
if (volumes[i] != 'on') {
try {
console.log(`Removing volume: ${volumes[i]}`);
let volume = docker.getVolume(volumes[i]);
await volume.remove();
} catch (error) {
console.log(`Unable to remove volume: ${volumes[i]}`);
}
}
}
res.redirect("/volumes");
}
// docker.df(volume.Name).then((data) => {
// for (let key in data) {
// console.log(data[key]);
// }
// });

View file

@ -1,42 +0,0 @@
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false
});
const Server = sequelize.define('Server', {
// Model attributes are defined here
timezone: {
type: DataTypes.STRING,
allowNull: false
},
hwa: {
type: DataTypes.STRING
// allowNull defaults to true
},
media: {
type: DataTypes.STRING
// allowNull defaults to true
},
pgid: {
type: DataTypes.STRING
// allowNull defaults to true
},
puid: {
type: DataTypes.STRING
// allowNull defaults to true
}
});
async function syncModel() {
await sequelize.sync();
console.log('Server model synced');
}
syncModel();
module.exports = Server;

View file

@ -1,63 +0,0 @@
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false
});
const User = sequelize.define('User', {
// Model attributes are defined here
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
first_name: {
type: DataTypes.STRING,
allowNull: false
},
last_name: {
type: DataTypes.STRING
// allowNull defaults to true
},
username: {
type: DataTypes.STRING
// allowNull defaults to true
},
email: {
type: DataTypes.STRING
// allowNull defaults to true
},
password: {
type: DataTypes.STRING,
// allowNull: false
},
role: {
type: DataTypes.STRING
// allowNull defaults to true
},
group: {
type: DataTypes.STRING
// allowNull defaults to true
},
avatar: {
type: DataTypes.STRING
// allowNull defaults to true
},
UUID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
}
});
async function syncModel() {
await sequelize.sync();
console.log('User model synced');
}
syncModel();
module.exports = User;

256
database/models.js Normal file
View file

@ -0,0 +1,256 @@
import { Sequelize, DataTypes } from 'sequelize';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './database/db.sqlite',
logging: false,
});
export const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING
},
username: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false
},
role: {
type: DataTypes.STRING
},
group: {
type: DataTypes.STRING
},
avatar: {
type: DataTypes.STRING
},
lastLogin: {
type: DataTypes.STRING
},
UUID: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
}
});
export const Container = sequelize.define('Container', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
visibility: {
type: DataTypes.STRING
},
service: {
type: DataTypes.STRING
},
state: {
type: DataTypes.STRING
},
image: {
type: DataTypes.STRING
},
external_port: {
type: DataTypes.STRING
},
internal_port: {
type: DataTypes.STRING
},
ports: {
type: DataTypes.STRING
},
volumes: {
type: DataTypes.STRING
},
environment_variables: {
type: DataTypes.STRING
},
labels: {
type: DataTypes.STRING
},
IPv4: {
type: DataTypes.STRING
},
style: {
type: DataTypes.STRING
},
cpu: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
ram: {
// store the last 15 values from dockerContainerStats
type: DataTypes.STRING
},
});
export const Permission = sequelize.define('Permission', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
containerName: {
type: DataTypes.STRING,
},
containerID: {
type: DataTypes.STRING,
},
user: {
type: DataTypes.STRING,
allowNull: false
},
userID: {
type: DataTypes.STRING,
allowNull: false
},
install: {
type: DataTypes.STRING,
defaultValue: false
},
uninstall: {
type: DataTypes.STRING,
defaultValue: false
},
edit: {
type: DataTypes.STRING,
defaultValue: false
},
upgrade: {
type: DataTypes.STRING,
defaultValue: false
},
start: {
type: DataTypes.STRING,
defaultValue: false
},
stop: {
type: DataTypes.STRING,
defaultValue: false
},
restart: {
type: DataTypes.STRING,
defaultValue: false
},
pause: {
type: DataTypes.STRING,
defaultValue: false
},
logs: {
type: DataTypes.STRING,
defaultValue: false
},
hide: {
type: DataTypes.STRING,
defaultValue: false
},
reset_view: {
type: DataTypes.STRING,
defaultValue: false
},
view: {
type: DataTypes.STRING,
defaultValue: false
},
});
export const Syslog = sequelize.define('Syslog', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
user: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING
},
event: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: false
},
ip : {
type: DataTypes.STRING
},
});
export const Notification = sequelize.define('Notification', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
title: {
type: DataTypes.STRING
},
message: {
type: DataTypes.STRING
},
icon: {
type: DataTypes.STRING,
},
color: {
type: DataTypes.STRING,
},
read: {
type: DataTypes.STRING,
},
createdAt : {
type: DataTypes.STRING
},
createdBy : {
type: DataTypes.STRING
},
});
export const Settings = sequelize.define('Settings', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
key: {
type: DataTypes.STRING,
allowNull: false
},
value: {
type: DataTypes.STRING,
allowNull: false
}
});
export const Variables = sequelize.define('Variables', {
find: {
type: DataTypes.STRING,
},
replace: {
type: DataTypes.STRING,
}
});

View file

@ -1,205 +0,0 @@
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
const yaml = require('js-yaml');
const { exec, execSync } = require("child_process");
const { docker } = require('./system');
var DockerodeCompose = require('dockerode-compose');
module.exports.install = async function (data) {
console.log(`[Start of install function]`);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
req.app.locals.caddy = 'enabled';
}
let docker_volumes = [];
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
console.log(`Downloaded stackfile: ${image}`);
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
let services = Object.keys(stackfile.services);
for ( let i = 0; i < services.length; i++ ) {
try {
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
} catch { console.log('no env') }
}
} else {
let compose_file = `version: '3'`;
compose_file += `\nservices:`
compose_file += `\n ${service_name}:`
compose_file += `\n container_name: ${name}`;
compose_file += `\n image: ${image}`;
// Command
if (command_check == 'on') {
compose_file += `\n command: ${command}`
}
// Network mode
if (net_mode == 'host') {
compose_file += `\n network_mode: 'host'`
}
else if (net_mode != 'host' && net_mode != 'docker') {
compose_file += `\n network_mode: '${net_mode}'`
}
// Restart policy
if (restart_policy != '') {
compose_file += `\n restart: ${restart_policy}`
}
// Ports
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
for (let i = 0; i < 6; i++) {
if (data[`port${i}`] == 'on') {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
}
// Volumes
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
}
// Environment variables
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
compose_file += `\n environment:`
}
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Add labels
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
compose_file += `\n labels:`
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Add privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
if (data[`env_${i}_name`] == 'DRINODE') {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
}
}
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
} catch { console.log('error creating directory or compose file') }
try {
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
await compose.pull();
await compose.up();
})();
} catch { console.log('error running compose file')}
}
}
module.exports.uninstall = async function (data) {
if (data.confirm == 'Yes') {
var containerName = docker.getContainer(`${data.service_name}`);
try {
containerName.stop(function (err, data) {
if (data) {
containerName.remove(function (err, data) {
});
}
});
} catch {
containerName.remove(function (err, data) {
});
}
}
}

View file

@ -1,199 +0,0 @@
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
const yaml = require('js-yaml');
const { exec, execSync } = require("child_process");
const { docker } = require('./system');
var DockerodeCompose = require('dockerode-compose');
module.exports.install = async function (data) {
console.log(`[Start of install function]`);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
if ((service_name.includes('caddy')) || (name.includes('caddy'))) {
req.app.locals.caddy = 'enabled';
}
let docker_volumes = [];
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
console.log(`Downloaded stackfile: ${image}`);
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
let services = Object.keys(stackfile.services);
for ( let i = 0; i < services.length; i++ ) {
try {
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
} catch { console.log('no env') }
}
} else {
let compose_file = `version: '3'`;
compose_file += `\nservices:`
compose_file += `\n ${service_name}:`
compose_file += `\n container_name: ${name}`;
compose_file += `\n image: ${image}`;
// Command
if (command_check == 'on') {
compose_file += `\n command: ${command}`
}
// Network mode
if (net_mode == 'host') {
compose_file += `\n network_mode: 'host'`
}
else if (net_mode != 'host' && net_mode != 'docker') {
compose_file += `\n network_mode: '${net_mode}'`
}
// Restart policy
if (restart_policy != '') {
compose_file += `\n restart: ${restart_policy}`
}
// Ports
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
for (let i = 0; i < 6; i++) {
if (data[`port${i}`] == 'on') {
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
}
}
}
// Volumes
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
}
// Environment variables
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
compose_file += `\n environment:`
}
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
}
}
// Add labels
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
compose_file += `\n labels:`
}
for (let i = 0; i < 12; i++) {
if (data[`label${i}`] == 'on') {
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
}
}
// Add privileged mode
if (data.privileged == 'on') {
compose_file += `\n privileged: true`
}
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
for (let i = 0; i < 12; i++) {
if (data[`env${i}`] == 'on') {
if (data[`env_${i}_name`] == 'DRINODE') {
compose_file += `\n deploy:`
compose_file += `\n resources:`
compose_file += `\n reservations:`
compose_file += `\n devices:`
compose_file += `\n - driver: nvidia`
compose_file += `\n count: 1`
compose_file += `\n capabilities: [gpu]`
}
}
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
} catch { console.log('error creating directory or compose file') }
try {
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
(async () => {
await compose.pull();
await compose.up();
})();
} catch { console.log('error running compose file')}
}
}
module.exports.uninstall = async function (data) {
if (data.confirm == 'Yes') {
console.log(`Uninstalling ${data.service_name}: ${data}`);
var containerName = docker.getContainer(`${data.service_name}`);
try {
await containerName.stop();
console.log(`Stopped ${data.service_name} container`);
} catch {
console.log(`Error stopping ${data.service_name} container`);
}
try {
await containerName.remove();
console.log(`Removed ${data.service_name} container`);
} catch {
console.log(`Error removing ${data.service_name} container`);
}
}
}

View file

@ -1,295 +0,0 @@
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats } = require('systeminformation');
var Docker = require('dockerode');
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
const { dashCard } = require('../components/dashCard');
const { Readable } = require('stream');
// export docker
module.exports.docker = docker;
module.exports.serverStats = async function () {
const cpuUsage = await currentLoad();
const ramUsage = await mem();
const netUsage = await networkStats();
const diskUsage = await fsSize();
const info = {
cpu: Math.round(cpuUsage.currentLoad),
ram: Math.round((ramUsage.active / ramUsage.total) * 100),
tx: netUsage[0].tx_bytes,
rx: netUsage[0].rx_bytes,
disk: diskUsage[0].use,
};
return info;
}
module.exports.containerList = async function () {
let card_list = '';
const data = await docker.listContainers({ all: true });
for (const container of data) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
// Get ports //////////////////////////
let ports_list = [];
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check : 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
} catch { console.log('no ports') }
for (let i = 0; i < 12; i++) {
if (ports_list[i] == undefined) {
let ports = {
check : '',
external: '',
internal: '',
protocol: ''
}
ports_list[i] = ports;
}
} /////////////////////////////////////
// Get volumes ////////////////////////
let volumes_list = [];
try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
let volumes = {
check : 'checked',
bind: value.split(':')[0],
container: value.split(':')[1],
readwrite: value.split(':')[2]
}
volumes_list.push(volumes);
}} catch { console.log('no volumes') }
for (let i = 0; i < 12; i++) {
if (volumes_list[i] == undefined) {
let volumes = {
check : '',
bind: '',
container: '',
readwrite: ''
}
volumes_list[i] = volumes;
}
} /////////////////////////////////////
// Get environment variables.
let environment_variables = [];
try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
let env = {
check : 'checked',
name: value.split('=')[0],
default: value.split('=')[1]
}
environment_variables.push(env);
}} catch { console.log('no env') }
for (let i = 0; i < 12; i++) {
if (environment_variables[i] == undefined) {
let env = {
check : '',
name: '',
default: ''
}
environment_variables[i] = env;
}
}
// Get labels.
let labels = [];
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
let label = {
check : 'checked',
name: key,
value: value
}
labels.push(label);
}
for (let i = 0; i < 12; i++) {
if (labels[i] == undefined) {
let label = {
check : '',
name: '',
value: ''
}
labels[i] = label;
}
}
let container_info = {
name: container.Names[0].slice(1),
service: service,
id: container.Id,
state: container.State,
image: container.Image,
external_port: ports_list[0].external || 0,
internal_port: ports_list[0].internal || 0,
ports: ports_list,
volumes: volumes_list,
environment_variables: environment_variables,
labels: labels,
}
let dockerCard = dashCard(container_info);
card_list += dockerCard;
}
}
return card_list;
}
module.exports.containerStats = async function () {
let container_stats = [];
const data = await docker.listContainers({ all: true });
for (const container of data) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
const stats = await dockerContainerStats(container.Id);
let container_stat = {
name: container.Names[0].slice(1),
cpu: Math.round(stats[0].cpuPercent),
ram: Math.round(stats[0].memPercent)
}
//push stats to an array
container_stats.push(container_stat);
}
}
return container_stats;
}
module.exports.containerAction = async function (data) {
let { user, role, action, container, state } = data;
console.log(`${user} wants to: ${action} ${container}`);
if (role == 'admin') {
var containerName = docker.getContainer(container);
if ((action == 'start') && (state == 'stopped')) {
containerName.start();
} else if ((action == 'start') && (state == 'paused')) {
containerName.unpause();
} else if ((action == 'stop') && (state != 'stopped')) {
containerName.stop();
} else if ((action == 'pause') && (state == 'running')) {
containerName.pause();
} else if ((action == 'pause') && (state == 'paused')) {
containerName.unpause();
} else if (action == 'restart') {
containerName.restart();
}
} else {
console.log('User is not an admin');
}
}
module.exports.containerExec = async function (data) {
let { container, command } = data;
var containerName = docker.getContainer(container);
var options = {
Cmd: ['/bin/sh', '-c', command],
AttachStdout: true,
AttachStderr: true,
Tty: true
};
containerName.exec(options, function (err, exec) {
if (err) return;
exec.start(function (err, stream) {
if (err) return;
containerName.modem.demuxStream(stream, process.stdout, process.stderr);
exec.inspect(function (err, data) {
if (err) return;
});
});
});
}
module.exports.containerLogs = function (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = {
follow: false,
stdout: true,
stderr: false,
timestamps: false
};
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) {
reject(err);
return;
}
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};

969
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,29 @@
{
"name": "dweeb-ui",
"version": "1.0.0",
"main": "app.js",
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.0",
"child_process": "^1.0.2",
"dockerode": "^4.0.0",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"js-yaml": "^4.1.0",
"sequelize": "^6.35.1",
"socket.io": "^4.6.1",
"sqlite3": "^5.1.6",
"systeminformation": "^5.21.20"
"name": "dweebui",
"version": "0.60",
"description": "Free and Open-Source WebUI For Managing Your Containers.",
"main": "server.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"description": ""
"keywords": [],
"author": "lllllllillllllillll",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.12",
"bcrypt": "^5.1.1",
"dockerode": "^4.0.2",
"dockerode-compose": "^1.4.0",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-session": "^1.18.0",
"memorystore": "^1.6.7",
"multer": "^1.4.5-lts.1",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7",
"systeminformation": "^5.22.9",
"yaml": "^2.4.2"
}
}

View file

@ -2,7 +2,7 @@
.meter {
box-sizing: content-box;
height: 15px; /* Can be anything */
height: 15px;
margin-left: auto;
margin-right: auto;
position: relative;
@ -83,6 +83,10 @@
.blue > span {
background-image: linear-gradient(#2478f5, #22017e);
}
.purple > span {
background-image: linear-gradient(#bd14d3, #670370);
}
.nostripes > span > span,
.nostripes > span::after {

View file

@ -6036,7 +6036,7 @@ fieldset:disabled .btn {
color: var(--tblr-alert-color);
background-color: var(--tblr-alert-bg);
border: var(--tblr-alert-border);
border-radius: var(--tblr-alert-border-radius)
border-radius: var(--tblr-alert-border-radius);
}
.alert-heading {
@ -11462,47 +11462,38 @@ fieldset:disabled .btn {
}
.column-gap-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -12910,17 +12901,14 @@ fieldset:disabled .btn {
}
.columns-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
@ -13821,47 +13809,38 @@ fieldset:disabled .btn {
}
.column-gap-sm-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-sm-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-sm-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-sm-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-sm-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-sm-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-sm-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-sm-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-sm-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -13878,17 +13857,14 @@ fieldset:disabled .btn {
}
.columns-sm-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-sm-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-sm-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -14790,47 +14766,38 @@ fieldset:disabled .btn {
}
.column-gap-md-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-md-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-md-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-md-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-md-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-md-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-md-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-md-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-md-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -14847,17 +14814,14 @@ fieldset:disabled .btn {
}
.columns-md-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-md-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-md-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -15759,47 +15723,38 @@ fieldset:disabled .btn {
}
.column-gap-lg-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-lg-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-lg-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-lg-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-lg-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-lg-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-lg-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-lg-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-lg-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -15816,17 +15771,14 @@ fieldset:disabled .btn {
}
.columns-lg-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-lg-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-lg-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -16728,47 +16680,38 @@ fieldset:disabled .btn {
}
.column-gap-xl-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-xl-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-xl-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-xl-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-xl-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-xl-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-xl-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-xl-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-xl-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -16785,17 +16728,14 @@ fieldset:disabled .btn {
}
.columns-xl-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-xl-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-xl-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -17697,47 +17637,38 @@ fieldset:disabled .btn {
}
.column-gap-xxl-0 {
-moz-column-gap: 0 !important;
column-gap: 0 !important
}
.column-gap-xxl-1 {
-moz-column-gap: .25rem !important;
column-gap: .25rem !important
}
.column-gap-xxl-2 {
-moz-column-gap: .5rem !important;
column-gap: .5rem !important
}
.column-gap-xxl-3 {
-moz-column-gap: 1rem !important;
column-gap: 1rem !important
}
.column-gap-xxl-4 {
-moz-column-gap: 1.5rem !important;
column-gap: 1.5rem !important
}
.column-gap-xxl-5 {
-moz-column-gap: 2rem !important;
column-gap: 2rem !important
}
.column-gap-xxl-6 {
-moz-column-gap: 3rem !important;
column-gap: 3rem !important
}
.column-gap-xxl-7 {
-moz-column-gap: 5rem !important;
column-gap: 5rem !important
}
.column-gap-xxl-8 {
-moz-column-gap: 8rem !important;
column-gap: 8rem !important
}
@ -17754,17 +17685,14 @@ fieldset:disabled .btn {
}
.columns-xxl-2 {
-moz-columns: 2 !important;
columns: 2 !important
}
.columns-xxl-3 {
-moz-columns: 3 !important;
columns: 3 !important
}
.columns-xxl-4 {
-moz-columns: 4 !important;
columns: 4 !important
}
}
@ -20293,11 +20221,11 @@ body[data-bs-theme=dark] .hide-theme-dark {
}
.alert {
--tblr-alert-color: var(--tblr-muted);
background: #fff;
--tblr-alert-color: var(--tblr-secondary);
--tblr-alert-bg: var(--tblr-surface);
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);
border-left: .25rem var(--tblr-border-style) var(--tblr-alert-color);
box-shadow: rgba(24, 36, 51, .04) 0 2px 4px 0
border-left: 0.25rem var(--tblr-border-style) var(--tblr-alert-color);
box-shadow: rgba(24, 36, 51, 0.04) 0 2px 4px 0;
}
.alert>:last-child {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

59
public/fonts/inter.css Normal file
View file

@ -0,0 +1,59 @@
/* Variable fonts usage:
:root { font-family: "Inter", sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
} */
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable.woff2?v=4.0') format('woff2');
}
@font-face {
font-family: InterVariable;
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url('InterVariable-Italic.woff2?v=4.0') format('woff2');
}
/* legacy name "Inter var" (Oct 2023) */
@font-face { font-family:'Inter var'; font-style:normal; font-weight:100 900; font-display:swap; src: url('InterVariable.woff2?v=4.0') format('woff2'); }
@font-face { font-family:'Inter var'; font-style:italic; font-weight:100 900; font-display:swap; src: url('InterVariable-Italic.woff2?v=4.0') format('woff2'); }
/* static fonts */
@font-face { font-family:Inter; font-style:normal; font-weight:100; font-display:swap; src:url("Inter-Thin.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:100; font-display:swap; src:url("Inter-ThinItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:200; font-display:swap; src:url("Inter-ExtraLight.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:200; font-display:swap; src:url("Inter-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:300; font-display:swap; src:url("Inter-Light.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:300; font-display:swap; src:url("Inter-LightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:400; font-display:swap; src:url("Inter-Regular.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:400; font-display:swap; src:url("Inter-Italic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:500; font-display:swap; src:url("Inter-Medium.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:500; font-display:swap; src:url("Inter-MediumItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:600; font-display:swap; src:url("Inter-SemiBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:600; font-display:swap; src:url("Inter-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:700; font-display:swap; src:url("Inter-Bold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:700; font-display:swap; src:url("Inter-BoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:800; font-display:swap; src:url("Inter-ExtraBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:800; font-display:swap; src:url("Inter-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:normal; font-weight:900; font-display:swap; src:url("Inter-Black.woff2?v=4.0") format("woff2"); }
@font-face { font-family:Inter; font-style:italic; font-weight:900; font-display:swap; src:url("Inter-BlackItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:100; font-display:swap; src:url("InterDisplay-Thin.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:100; font-display:swap; src:url("InterDisplay-ThinItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLight.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:200; font-display:swap; src:url("InterDisplay-ExtraLightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:300; font-display:swap; src:url("InterDisplay-Light.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:300; font-display:swap; src:url("InterDisplay-LightItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:400; font-display:swap; src:url("InterDisplay-Regular.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:400; font-display:swap; src:url("InterDisplay-Italic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:500; font-display:swap; src:url("InterDisplay-Medium.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:500; font-display:swap; src:url("InterDisplay-MediumItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:600; font-display:swap; src:url("InterDisplay-SemiBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:700; font-display:swap; src:url("InterDisplay-Bold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:700; font-display:swap; src:url("InterDisplay-BoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBold.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:800; font-display:swap; src:url("InterDisplay-ExtraBoldItalic.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:normal; font-weight:900; font-display:swap; src:url("InterDisplay-Black.woff2?v=4.0") format("woff2"); }
@font-face { font-family:InterDisplay; font-style:italic; font-weight:900; font-display:swap; src:url("InterDisplay-BlackItalic.woff2?v=4.0") format("woff2"); }

BIN
public/img/add to zip.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

3
public/img/dweebui.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="180" height="50" viewBox="0 0 180 50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g transform="translate(0 0)">
<rect width="180" height="50" rx="2" transform="translate(0 0)" fill="transparent"></rect><text transform="translate(8 43)" fill="rgba(33, 33, 33, 0.7)" font-size="40" font-family="Roboto-Bold,Roboto" ><tspan x="0" y="0">DweebUI</tspan></text>
</g></svg>

After

Width:  |  Height:  |  Size: 413 B

BIN
public/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -12,7 +12,7 @@
})((function () { 'use strict';
var themeStorageKey = "tablerTheme";
var defaultTheme = "light";
var defaultTheme = "dark";
var selectedTheme;
var params = new Proxy(new URLSearchParams(window.location.search), {
get: function get(searchParams, prop) {

355
public/js/htmx-sse.js Normal file
View file

@ -0,0 +1,355 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("sse", {
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
switch (name) {
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(evt.target)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close();
}
return;
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(evt.target);
registerSSE(evt.target);
}
}
});
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true });
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacySSEURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
function getLegacySSESwaps(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
var returnArr = [];
if (legacySSEValue != null) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "swap") {
returnArr.push(value[1]);
}
}
}
return returnArr;
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
if (sseSwapAttr) {
var sseEventNames = sseSwapAttr.split(",");
} else {
var sseEventNames = getLegacySSESwaps(child);
}
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim();
var listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return;
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
// swap the response into the DOM and trigger a notification
swap(child, event.data);
api.triggerEvent(elt, "htmx:sseMessage", event);
};
// Register the new listener
api.getInternalData(child).sseEventListener = listener;
source.addEventListener(sseEventName, listener);
}
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
var sseEventName = api.getAttributeValue(child, "hx-trigger");
if (sseEventName == null) {
return;
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
// remove the sse: prefix from here on out
sseEventName = sseEventName.substr(4);
var listener = function() {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
}
});
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
var sseURL = api.getAttributeValue(child, "sse-connect");
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
// handle legacy sse, remove for HTMX2
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
var sseURL = getLegacySSEURL(child);
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url);
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return;
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0;
var timeout = Math.random() * (2 ^ retryCount) * 500;
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
}, timeout);
}
};
source.onopen = function(evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
}
api.getInternalData(elt).sseEventSource = source;
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) {
source.close();
// source = null
return true;
}
}
return false;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = [];
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
result.push(node);
});
return result;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
var settleInfo = api.makeSettleInfo(elt);
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:beforeSettle');
});
// Handle settle tasks (with delay if requested)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
} else {
doSettle(settleInfo)();
}
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call();
});
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:afterSettle');
});
}
}
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null;
}
})();

1
public/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,198 +0,0 @@
// SOCKET IO
const socket = io({
auth: {
token: "abc"
}
});
// ON CONNECT EVENT
socket.on('connect', () => {
console.log('Connected');
});
// SELECT METRICS ELEMENTS
const cpuText = document.getElementById('cpu-text');
const cpuBar = document.getElementById('cpu-bar');
const ramText = document.getElementById('ram-text');
const ramBar = document.getElementById('ram-bar');
const netText = document.getElementById('net-text');
const netBar = document.getElementById('net-bar');
const diskText = document.getElementById('disk-text');
const diskBar = document.getElementById('disk-bar');
const dockerCards = document.getElementById('cards');
const logViewer = document.getElementById('logView');
//Update usage bars
socket.on('metrics', (data) => {
let {cpu, ram, tx, rx, disk} = data;
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
tx = Math.round(tx / 1024 / 1024);
rx = Math.round(rx / 1024 / 1024);
netText.innerHTML = `<span>Down: ${rx}MB</span><span> Up: ${tx}MB</span>`;
netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
});
function drawCharts(name, cpu_array, ram_array) {
var elements = document.querySelectorAll(`${name}`);
Array.from(elements).forEach(function(element) {
if (window.ApexCharts) {
new ApexCharts(element, {
chart: {
type: "line",
fontFamily: 'inherit',
height: 40.0,
sparkline: {
enabled: true
},
animations: {
enabled: false
}
},
fill: {
opacity: 1
},
stroke: {
width: [2, 1],
dashArray: [0, 3],
lineCap: "round",
curve: "smooth"
},
series: [{
name: "CPU",
data: cpu_array
}, {
name: "RAM",
data: ram_array
}],
tooltip: {
theme: 'dark'
},
grid: {
strokeDashArray: 4
},
xaxis: {
labels: {
padding: 0
},
tooltip: {
enabled: false
},
type: 'datetime'
},
yaxis: {
labels: {
padding: 4
}
},
labels: [
'2020-06-20', '2020-06-21', '2020-06-22', '2020-06-23', '2020-06-24', '2020-06-25', '2020-06-26', '2020-06-27', '2020-06-28', '2020-06-29', '2020-06-30', '2020-07-01', '2020-07-02', '2020-07-03', '2020-07-04', '2020-07-05', '2020-07-06', '2020-07-07', '2020-07-08', '2020-07-09', '2020-07-10', '2020-07-11', '2020-07-12', '2020-07-13', '2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17', '2020-07-18', '2020-07-19'
],
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
legend: {
show: false
}
}).render();
}
});
}
// container button actions
function buttonAction(button) {
socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
}
let containerLogs;
function viewLogs(button) {
if (button.name != 'refresh') {
containerLogs = button.name;
}
socket.emit('logs', {container: containerLogs});
}
socket.on('logString', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});
socket.on('cards', (data) => {
console.log('cards deleted');
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
// check localStorage for items ending with _cpu and redraw the charts
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i).endsWith('_cpu')) {
let name = localStorage.key(i).split('_')[0];
let cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
let ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
drawCharts(`#${name}_chart`, cpu_array, ram_array);
}
}
});
socket.on('containerStats', (data) => {
let {name, cpu, ram} = data;
console.log(`drawing chart for ${name}`)
var cpu_array = JSON.parse(localStorage.getItem(`${name}_cpu`));
var ram_array = JSON.parse(localStorage.getItem(`${name}_ram`));
if (cpu_array == null) { cpu_array = Array(30).fill(0); }
if (ram_array == null) { ram_array = Array(30).fill(0); }
cpu_array.push(cpu);
ram_array.push(ram);
cpu_array = cpu_array.slice(-30);
ram_array = ram_array.slice(-30);
localStorage.setItem(`${name}_cpu`, JSON.stringify(cpu_array));
localStorage.setItem(`${name}_ram`, JSON.stringify(ram_array));
// replace the old chart with the new one
let chart = document.getElementById(`${name}_chart`);
if (chart) {
let newChart = document.createElement('div');
newChart.id = `${name}_chart`;
chart.parentNode.replaceChild(newChart, chart);
drawCharts(`#${name}_chart`, cpu_array, ram_array);
} else {
console.log(`Chart element with id ${name}_chart not found in the DOM`);
}
});
socket.on('install', (data) => {
console.log('added install card');
dockerCards.insertAdjacentHTML("afterend", data);
});

2020
public/libs/list.js/dist/list.js vendored Normal file

File diff suppressed because it is too large Load diff

1
public/libs/list.js/dist/list.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
public/libs/list.js/dist/list.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"file":"list.min.js","sources":["webpack://List/list.min.js"],"mappings":"AAAA","sourceRoot":""}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 68">
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#ffffff"/>
</svg>

Before

Width:  |  Height:  |  Size: 599 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 232 68">
<path d="M64.6 16.2C63 9.9 58.1 5 51.8 3.4 40 1.5 28 1.5 16.2 3.4 9.9 5 5 9.9 3.4 16.2 1.5 28 1.5 40 3.4 51.8 5 58.1 9.9 63 16.2 64.6c11.8 1.9 23.8 1.9 35.6 0C58.1 63 63 58.1 64.6 51.8c1.9-11.8 1.9-23.8 0-35.6zM33.3 36.3c-2.8 4.4-6.6 8.2-11.1 11-1.5.9-3.3.9-4.8.1s-2.4-2.3-2.5-4c0-1.7.9-3.3 2.4-4.1 2.3-1.4 4.4-3.2 6.1-5.3-1.8-2.1-3.8-3.8-6.1-5.3-2.3-1.3-3-4.2-1.7-6.4s4.3-2.9 6.5-1.6c4.5 2.8 8.2 6.5 11.1 10.9 1 1.4 1 3.3.1 4.7zM49.2 46H37.8c-2.1 0-3.8-1-3.8-3s1.7-3 3.8-3h11.4c2.1 0 3.8 1 3.8 3s-1.7 3-3.8 3z" fill="#fff"/>
<path d="M105.8 46.1c.4 0 .9.2 1.2.6s.6 1 .6 1.7c0 .9-.5 1.6-1.4 2.2s-2 .9-3.2.9c-2 0-3.7-.4-5-1.3s-2-2.6-2-5.4V31.6h-2.2c-.8 0-1.4-.3-1.9-.8s-.9-1.1-.9-1.9c0-.7.3-1.4.8-1.8s1.2-.7 1.9-.7h2.2v-3.1c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v3.1h3.4c.8 0 1.4.3 1.9.8s.8 1.2.8 1.9-.3 1.4-.8 1.8-1.2.7-1.9.7h-3.4v13c0 .7.2 1.2.5 1.5s.8.5 1.4.5c.3 0 .6-.1 1.1-.2.5-.2.8-.3 1.2-.3zm28-20.7c.8 0 1.5.3 2.1.8.5.5.8 1.2.8 2.1v20.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2-.8-.8-1.2-.8-2.1c-.8.9-1.9 1.7-3.2 2.4-1.3.7-2.8 1-4.3 1-2.2 0-4.2-.6-6-1.7-1.8-1.1-3.2-2.7-4.2-4.7s-1.6-4.3-1.6-6.9c0-2.6.5-4.9 1.5-6.9s2.4-3.6 4.2-4.8c1.8-1.1 3.7-1.7 5.9-1.7 1.5 0 3 .3 4.3.8 1.3.6 2.5 1.3 3.4 2.1 0-.8.3-1.5.8-2.1.5-.5 1.2-.7 2-.7zm-9.7 21.3c2.1 0 3.8-.8 5.1-2.3s2-3.4 2-5.7-.7-4.2-2-5.8c-1.3-1.5-3-2.3-5.1-2.3-2 0-3.7.8-5 2.3-1.3 1.5-2 3.5-2 5.8s.6 4.2 1.9 5.7 3 2.3 5.1 2.3zm32.1-21.3c2.2 0 4.2.6 6 1.7 1.8 1.1 3.2 2.7 4.2 4.7s1.6 4.3 1.6 6.9-.5 4.9-1.5 6.9-2.4 3.6-4.2 4.8c-1.8 1.1-3.7 1.7-5.9 1.7-1.5 0-3-.3-4.3-.9s-2.5-1.4-3.4-2.3v.3c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.5-.8-1.2-.8-2.1V18.9c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v10c.8-1 1.8-1.8 3.2-2.5 1.3-.7 2.8-1 4.3-1zm-.7 21.3c2 0 3.7-.8 5-2.3s2-3.5 2-5.8-.6-4.2-1.9-5.7-3-2.3-5.1-2.3-3.8.8-5.1 2.3-2 3.4-2 5.7.7 4.2 2 5.8c1.3 1.6 3 2.3 5.1 2.3zm23.6 1.9c0 .8-.3 1.5-.8 2.1s-1.3.8-2.1.8-1.5-.3-2-.8-.8-1.3-.8-2.1V18.9c0-.8.3-1.5.8-2.1s1.3-.8 2.1-.8 1.5.3 2 .8.8 1.3.8 2.1v29.7zm29.3-10.5c0 .8-.3 1.4-.9 1.9-.6.5-1.2.7-2 .7h-15.8c.4 1.9 1.3 3.4 2.6 4.4 1.4 1.1 2.9 1.6 4.7 1.6 1.3 0 2.3-.1 3.1-.4.7-.2 1.3-.5 1.8-.8.4-.3.7-.5.9-.6.6-.3 1.1-.4 1.6-.4.7 0 1.2.2 1.7.7s.7 1 .7 1.7c0 .9-.4 1.6-1.3 2.4-.9.7-2.1 1.4-3.6 1.9s-3 .8-4.6.8c-2.7 0-5-.6-7-1.7s-3.5-2.7-4.6-4.6-1.6-4.2-1.6-6.6c0-2.8.6-5.2 1.7-7.2s2.7-3.7 4.6-4.8 3.9-1.7 6-1.7 4.1.6 6 1.7 3.4 2.7 4.5 4.7c.9 1.9 1.5 4.1 1.5 6.3zm-12.2-7.5c-3.7 0-5.9 1.7-6.6 5.2h12.6v-.3c-.1-1.3-.8-2.5-2-3.5s-2.5-1.4-4-1.4zm30.3-5.2c1 0 1.8.3 2.4.8.7.5 1 1.2 1 1.9 0 1-.3 1.7-.8 2.2-.5.5-1.1.8-1.8.7-.5 0-1-.1-1.6-.3-.2-.1-.4-.1-.6-.2-.4-.1-.7-.1-1.1-.1-.8 0-1.6.3-2.4.8s-1.4 1.3-1.9 2.3-.7 2.3-.7 3.7v11.4c0 .8-.3 1.5-.8 2.1-.5.6-1.2.8-2.1.8s-1.5-.3-2.1-.8c-.5-.6-.8-1.3-.8-2.1V28.8c0-.8.3-1.5.8-2.1.5-.6 1.2-.8 2.1-.8s1.5.3 2.1.8c.5.6.8 1.3.8 2.1v.6c.7-1.3 1.8-2.3 3.2-3 1.3-.7 2.8-1 4.3-1z" fill-rule="evenodd" clip-rule="evenodd" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Some files were not shown because too many files have changed in this diff Show more