Compare commits

...

310 commits
v0.01 ... 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
lllllllillllllillll
649c7287a2
Merge pull request #32 from lllllllillllllillll/dev
v0.07
2023-12-08 01:36:14 -08:00
lllllllillllllillll
e35dbb5466
v0.07 release date 2023-12-08 01:32:47 -08:00
lllllllillllllillll
ea2e0f9fd8
removed redis 2023-12-08 01:26:46 -08:00
lllllllillllllillll
b563f8bd01
v0.07 2023-12-08 01:26:02 -08:00
lllllllillllllillll
02dce5eb09
v0.07 2023-12-08 01:22:55 -08:00
lllllllillllllillll
36cc3ffb88
Delete LICENSE 2023-12-07 19:34:34 -08:00
lllllllillllllillll
3d110c137c
Delete LICENSE 2023-12-07 19:34:01 -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
lllllllillllllillll
b49545965a
Bump systeminformation from 5.21.17 to 5.21.20
#30
2023-12-06 01:49:08 -08:00
lllllllillllllillll
58a006e48e
Update README.md 2023-12-05 19:39:32 -08:00
lllllllillllllillll
0a5681d745
removed redis 2023-12-05 18:11:51 -08:00
lllllllillllllillll
69f2eec789
Update CHANGELOG.md
added: removed redis
2023-12-05 01:02:58 -08:00
lllllllillllllillll
4eeb8f6b25
removed redis 2023-12-04 23:21:27 -08:00
lllllllillllllillll
8a8d28ad8f
container metrics fix 2023-12-04 01:37:06 -08:00
lllllllillllllillll
93ba77b4e3
v0.07 ( dev )
## v0.07 ( dev )
* View container logs.
* Improved uninstall function and form id fix.
* WebUI Port can be changed in compose.yml
* Code clean-up.
* Updated dependencies (redis-connect and systeminformation).
2023-12-04 01:18:45 -08:00
lllllllillllllillll
ab909bbe0e
uninstall fix
improved uninstall function and fixed duplicate form id
2023-12-04 00:06:46 -08:00
lllllllillllllillll
d73edb3ed1
Delete controllers/login.js 2023-12-03 16:22:53 -08:00
lllllllillllllillll
6f83ebef2e
Delete controllers/site_actions.js 2023-12-03 16:22:36 -08:00
lllllllillllllillll
234bcd7afa
Delete controllers/register.js 2023-12-03 16:22:22 -08:00
lllllllillllllillll
1508ae41c2
Delete controllers/logout.js 2023-12-03 16:22:10 -08:00
lllllllillllllillll
da0a5b8401
Viewable container logs
You can now view a containers logs. 
Code clean-up.
2023-12-03 14:24:30 -08:00
lllllllillllllillll
fb3fb34532
Delete functions/system_information.js 2023-11-26 17:53:25 -08:00
lllllllillllllillll
ca8a40c065
removed snake_case from app.js
renamed function system_information.js to system.js
2023-11-26 17:27:10 -08:00
lllllllillllllillll
ef2157af1f
updated compose and credits 2023-11-26 17:23:50 -08:00
lllllllillllllillll
fe5359ccf4
env.PORT
DweebUI port can be changed from compose file.
2023-11-26 17:18:43 -08:00
lllllllillllllillll
b1d9ff38b4
removed "no frameworks" 2023-11-25 00:32:34 -08:00
lllllllillllllillll
1a546999b9
systeminformation 5.21.17 to 5.21.18
last commit included update to connect-redis 7.1.0
systeminformation 5.21.17 to 5.21.18
2023-11-25 00:29:22 -08:00
lllllllillllllillll
392c5104f4
Add files via upload 2023-11-25 00:24:53 -08:00
lllllllillllllillll
086fd9667f
Merge pull request #25 from lllllllillllllillll/dev
v0.06
2023-11-24 01:05:11 -08:00
lllllllillllllillll
1893be7efc
v0.06 2023-11-24 00:54:21 -08:00
lllllllillllllillll
1bc322fa79
v0.06
Updated README and CHANGELOG
Connect-redis needed to be rolled back from dependabots pull request.
2023-11-24 00:52:49 -08:00
lllllllillllllillll
b05b82fca8
v0.06 2023-11-24 00:46:05 -08:00
lllllllillllllillll
2c7c4876a7
v0.06 fix
removed "-dev" from image
2023-11-24 00:44:24 -08:00
lllllllillllllillll
557d04069b
v0.06
* Removed Caddy
* Proxy Manager UI hidden unless env = enabled
* Removed hardcoded passwords for Redis
2023-11-24 00:41:13 -08:00
lllllllillllllillll
05e009b821
revert dependabot change
Had to update and test each dependency. 
Updating connect-redis to 7.1.0 breaks app.
Removed comments from Dockerfile
2023-11-22 17:13:56 -08:00
lllllllillllllillll
1ffdf156bf
Merge pull request #24 from lllllllillllllillll/dependabot/npm_and_yarn/redis-4.6.11
Bump redis from 4.6.10 to 4.6.11
2023-11-20 17:43:10 -08:00
dependabot[bot]
c76c7f854c
Bump redis from 4.6.10 to 4.6.11
Bumps [redis](https://github.com/redis/node-redis) from 4.6.10 to 4.6.11.
- [Release notes](https://github.com/redis/node-redis/releases)
- [Changelog](https://github.com/redis/node-redis/blob/master/CHANGELOG.md)
- [Commits](https://github.com/redis/node-redis/compare/redis@4.6.10...redis@4.6.11)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:20:14 +00:00
lllllllillllllillll
4d36b7dd3d
Merge pull request #22 from lllllllillllllillll/dependabot/npm_and_yarn/dockerode-4.0.0
Bump dockerode from 3.3.5 to 4.0.0
2023-11-19 23:36:20 -08:00
lllllllillllllillll
3fab643988
Merge pull request #21 from lllllllillllllillll/dependabot/npm_and_yarn/sequelize-6.35.1
Bump sequelize from 6.34.0 to 6.35.1
2023-11-19 23:35:28 -08:00
lllllllillllllillll
4130e0dd26
Merge pull request #20 from lllllllillllllillll/dependabot/npm_and_yarn/systeminformation-5.21.17
Bump systeminformation from 5.21.15 to 5.21.17
2023-11-19 23:35:18 -08:00
lllllllillllllillll
d471072b8d
Merge pull request #19 from lllllllillllllillll/dependabot/docker/node-21-alpine
Bump node from 20-alpine to 21-alpine
2023-11-19 23:32:03 -08:00
dependabot[bot]
01c95ce75d
Bump dockerode from 3.3.5 to 4.0.0
Bumps [dockerode](https://github.com/apocas/dockerode) from 3.3.5 to 4.0.0.
- [Release notes](https://github.com/apocas/dockerode/releases)
- [Commits](https://github.com/apocas/dockerode/compare/v3.3.5...v4.0.0)

---
updated-dependencies:
- dependency-name: dockerode
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 07:30:26 +00:00
lllllllillllllillll
cab98aab86
Merge pull request #23 from lllllllillllllillll/dependabot/npm_and_yarn/connect-redis-7.1.0
Bump connect-redis from 6.1.3 to 7.1.0
2023-11-19 23:29:47 -08:00
dependabot[bot]
578ffdff35
Bump connect-redis from 6.1.3 to 7.1.0
Bumps [connect-redis](https://github.com/tj/connect-redis) from 6.1.3 to 7.1.0.
- [Release notes](https://github.com/tj/connect-redis/releases)
- [Commits](https://github.com/tj/connect-redis/compare/v6.1.3...v7.1.0)

---
updated-dependencies:
- dependency-name: connect-redis
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-19 23:19:25 +00:00
dependabot[bot]
4a27bf1798
Bump sequelize from 6.34.0 to 6.35.1
Bumps [sequelize](https://github.com/sequelize/sequelize) from 6.34.0 to 6.35.1.
- [Release notes](https://github.com/sequelize/sequelize/releases)
- [Commits](https://github.com/sequelize/sequelize/compare/v6.34.0...v6.35.1)

---
updated-dependencies:
- dependency-name: sequelize
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-19 23:18:48 +00:00
dependabot[bot]
5ea8f0cb3f
Bump systeminformation from 5.21.15 to 5.21.17
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.21.15 to 5.21.17.
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.21.15...v5.21.17)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-19 23:18:29 +00:00
dependabot[bot]
c188a74454
Bump node from 20-alpine to 21-alpine
Bumps node from 20-alpine to 21-alpine.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-19 23:18:10 +00:00
lllllllillllllillll
5700266156
Merge pull request #18 from gaby/main
Update dependabot.yml
2023-11-19 15:17:34 -08:00
Juan Calderon-Perez
1887d756c4
Update dependabot.yml 2023-11-19 18:12:08 -05:00
lllllllillllllillll
53ea6a8802
Merge pull request #17 from gaby/docker-fixes
Dockerfile updates and Fixes to CI process
2023-11-19 10:48:22 -08:00
Juan Calderon-Perez
eceea4fd75
Remove arm/v7 2023-11-19 12:10:17 -05:00
Juan Calderon-Perez
83e3cff12f
Add linux/arm/v7 again 2023-11-19 12:06:42 -05:00
Juan Calderon-Perez
bf284d9bb2
Lets try this 2023-11-19 12:04:28 -05:00
Juan Calderon-Perez
b759017365
Remove unsupported ARM platform 2023-11-19 11:55:21 -05:00
Juan Calderon-Perez
eee0ab7f1f
Use debian-slim for Docker image 2023-11-19 11:53:26 -05:00
Juan Calderon-Perez
8a009932c0
Update docker.yml 2023-11-19 11:06:22 -05:00
Juan Calderon-Perez
7ef52805ca
Restore original Dockerfile, fix CI stage 2023-11-19 11:00:31 -05:00
Juan Calderon-Perez
569559a8fd
Simplify Dockerfile 2023-11-19 10:51:22 -05:00
lllllllillllllillll
e1d1df076e
Update Dockerfile 2023-11-19 01:33:48 -08:00
lllllllillllllillll
06a12259e6
Update Dockerfile 2023-11-18 22:20:44 -08:00
lllllllillllllillll
2eafda0c09
removed run commands 2023-11-18 22:17:58 -08:00
lllllllillllllillll
36d769c660
Merge pull request #16 from gaby/cicd
Support for Multi-platform images with GitHub Actions
2023-11-18 21:29:53 -08:00
Juan Calderon-Perez
da7867d591
Support for Multi-platform images with GitHub Actions 2023-11-18 20:01:18 -05:00
lllllllillllllillll
4c12fe184a
Update README.md
* [ ] VPN, VPS, and Firewall Toggles. (planned)
* [ ] Offline Mode. (planned)
2023-11-17 17:22:07 -08:00
lllllllillllllillll
9f138b0858
nowwww I fixed it 2023-11-17 16:42:17 -08:00
lllllllillllllillll
ed1ac6d758
removed :ro 2023-11-17 16:41:45 -08:00
lllllllillllllillll
e6984f0f05
Fixed-ish
Fixed issue with caddy volumes.
2023-11-17 16:39:54 -08:00
lllllllillllllillll
4f611b3526
I think I fixed it
the caddy volumes that is
2023-11-17 16:38:28 -08:00
lllllllillllllillll
afd547b488
Delete caddyfiles/sites/folder.txt 2023-11-17 15:12:09 -08:00
lllllllillllllillll
f3a65250cc
Create folder.txt 2023-11-17 15:11:53 -08:00
lllllllillllllillll
06a6550759
Add files via upload 2023-11-17 00:36:39 -08:00
lllllllillllllillll
829a698721
Update README.md 2023-11-17 00:34:32 -08:00
lllllllillllllillll
644f8b8a89
stars shield 2023-11-17 00:29:01 -08:00
lllllllillllllillll
8ac19ada26
Merge pull request #12 from lllllllillllllillll/dev
dev to main: v0.05 release
2023-11-17 00:17:34 -08:00
lllllllillllllillll
83cd7ce558
Merge branch 'main' into dev 2023-11-17 00:16:41 -08:00
lllllllillllllillll
4f5166456b
Update README.md 2023-11-16 23:46:19 -08:00
lllllllillllllillll
43a8c62099
v0.05 release date 2023-11-16 22:48:03 -08:00
lllllllillllllillll
cca19bfaeb
image size 2023-11-16 21:36:11 -08:00
lllllllillllllillll
0df303d385
Add files via upload 2023-11-16 21:20:25 -08:00
lllllllillllllillll
02a9cb5862
screenshots of v0.05 2023-11-16 20:09:27 -08:00
lllllllillllllillll
c2c4fcca89
v0.05
* Environment Variables and Labels are now unchecked by default.
* Support for Docker volumes.
* Fixed app uninstall.
* Fixed Proxy Manager.
* Updated functions to ignore the three DweebUI containers: DweebUI, DweebCache(redis), and DweebProxy(caddy).
* Visual updates: Tabs for networks, images, and volumes. Added 'update' option in container drop-down.
* Updated main.js to prevent javascript errors.
* Fix for templates using 'set' instead of 'default' in environment variables.
* Fixes for templates with no volumes or no labels.
* New README.md.
* New screenshots.
* Automatically persists data in docker volumes if there is no bind mount.
2023-11-16 18:01:24 -08:00
lllllllillllllillll
4cf7919056
new screenshots
new README.md
new CHANGELOG.md
new screenshots
2023-11-16 17:59:19 -08:00
lllllllillllllillll
3c41aaffa6
Update README.md 2023-11-12 16:52:37 -08:00
lllllllillllllillll
19f9e65755
Removed Caddy Proxy Manager.md
Will be fixed soon.
2023-11-12 16:52:06 -08:00
lllllllillllllillll
6ec890e0bc
v0.05 pre-release
When installing an app, Environment variables and labels are unchecked by default.
Added 'update' option to container drop down.
Fixed App Uninstall
Updated container_stats to ignore DweebUI and DweebCache
Updated main.js to fix javascript errors
2023-11-12 13:30:56 -08:00
lllllllillllllillll
39bb0485b9
Merge pull request #9 from lllllllillllllillll/dev
Merge dev into main for v0.04
2023-11-11 00:06:33 -08:00
lllllllillllllillll
0659999d56
v0.04.md
updated instructions for setup script or docker
2023-11-11 00:04:36 -08:00
lllllllillllllillll
cb7f9e5ab5
v0.04
## v0.04 (Nov 11th 2023)
* Docker Image and Compose file available.
* The containers DweebUI and DweebCache are hidden from the dashboard.
* Default icon for containers.
* Fixed missing information in container details/edit modals (Ports, Env, Volumes, Labels).
2023-11-11 00:00:48 -08:00
lllllllillllllillll
5d91cd50e4
v0.04
Displays default icon if one isn't found
Hid the containers DweebUI and DweebCache
New Docker Image and Compose file.
Fixed missing information viewing container details
2023-11-10 23:55:07 -08:00
lllllllillllllillll
e01b573673
added volume for dweebui 2023-11-10 17:54:40 -08:00
lllllllillllllillll
f0d78d201a
Working docker compose
Working but using hard coded passwords.
2023-11-06 19:08:35 -08:00
lllllllillllllillll
cf0c9dda4b
Hiding Dweeb Containers
* default images for dashCards.
* hiding DweebUI and DweebCache containers from displaying on dashboard.
2023-11-06 01:13:06 -08:00
lllllllillllllillll
2a4433c3e9
v0.04
Updated instructions for building Docker image
2023-11-05 18:25:18 -08:00
lllllllillllllillll
9f3cb150a7
Starting v0.04
First build of Docker image. Barely works, very buggy.
2023-11-05 18:19:17 -08:00
lllllllillllllillll
d7f75bf63b
v0.04 2023-11-05 18:16:06 -08:00
lllllllillllllillll
511d6fab14
v0.04 2023-11-05 18:14:03 -08:00
lllllllillllllillll
8621e122c4
Merge pull request #6 from lllllllillllllillll/dev
v0.03
2023-11-05 01:39:25 -07:00
lllllllillllllillll
009765f687
adjusted date.md 2023-11-05 01:27:09 -07:00
lllllllillllllillll
e48efe3ea1
Add files via upload 2023-11-04 23:41:18 -07:00
lllllllillllllillll
4c5ee56cf5
Add files via upload 2023-11-04 18:35:02 -07:00
lllllllillllllillll
d73e87b897
v0.03 changes 2023-11-04 12:02:12 -07:00
lllllllillllllillll
071ed3e515
Updated for v0.03 2023-11-04 11:59:15 -07:00
lllllllillllllillll
2a2f4331d6
v0.03
* Container graphs now load instantly
* Working net indicator
* Redis now installed as docker container
2023-11-04 10:56:08 -07:00
lllllllillllllillll
a72b0958d2
Merge pull request #5 from lllllllillllllillll/main
Merge updated readme and changelog
2023-11-01 18:14:12 -07:00
lllllllillllllillll
2b54fefb04
Delete controllers/app_actions.js 2023-11-01 01:06:03 -07:00
lllllllillllllillll
06c3789740
Update CHANGELOG.md 2023-11-01 01:05:22 -07:00
lllllllillllllillll
a6b3819368
Update README.md 2023-11-01 00:58:49 -07:00
lllllllillllllillll
94cd1293fa
Delete functions/systeminformation.js 2023-11-01 00:56:49 -07:00
lllllllillllllillll
9287c643ca
Add files via upload 2023-11-01 00:55:38 -07:00
lllllllillllllillll
e2463ebc47
Merge pull request #3 from lllllllillllllillll/dev
0.02
2023-11-01 00:54:17 -07:00
lllllllillllllillll
9597df2524
Add files via upload 2023-10-31 21:21:31 -07:00
lllllllillllllillll
0626792b0d
Add files via upload 2023-10-31 21:18:10 -07:00
lllllllillllllillll
ca06c09710
fixed variable "name" -> "user" 2023-10-29 18:59:42 -07:00
lllllllillllllillll
dc5a456269
significant code clean-up
Cleaned up the code in app.js.
Started moving code into /functions.
2023-10-29 17:38:17 -07:00
lllllllillllllillll
46fb7e0faf
working usage graphs for containers.
clunky af
2023-10-25 00:39:52 -07:00
lllllllillllllillll
c063a5a0e8
semi-working container graphs
It's pretty buggy, but it works.
2023-10-21 23:23:54 -07:00
lllllllillllllillll
2942a96d46
building stackfile support. updated template.json 2023-10-18 23:43:16 -07:00
lllllllillllllillll
e4f2902f8b
Fix dashCard port. Started work on stackfiles 2023-10-18 13:44:59 -07:00
lllllllillllllillll
9b0be82d3c
Update FUNDING.yml 2023-10-18 01:47:37 -07:00
lllllllillllllillll
8ff9b9f393
renamed appCard fields
renamed appCard fields. added model for server settings.
2023-10-18 01:12:02 -07:00
lllllllillllllillll
ad9e13f4a7
added screenshot.md 2023-10-17 21:24:11 -07:00
lllllllillllllillll
b4bf755874
Add files via upload 2023-10-17 21:20:41 -07:00
lllllllillllllillll
454106a4a2
Create CHANGELOG.md 2023-10-17 01:30:39 -07:00
lllllllillllllillll
49c311378e
Add files via upload
Updated leantime and dashdot. Added privileged mode support. change vnc and vpn buttons to white
2023-10-17 01:26:04 -07:00
lllllllillllllillll
9f6533e857
Update README.md 2023-10-15 17:27:48 -07:00
149 changed files with 13099 additions and 5042 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: lllllllillllllillll
patreon: DweebUI

20
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "🤖 Dependencies"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
labels:
- "🤖 Dependencies"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
labels:
- "🤖 Dependencies"

67
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: Docker
on:
push:
branches:
- "main"
paths-ignore:
- "**.md"
- LICENSE
- "compose.yml"
- ".github/dependabot.yml"
pull_request:
branches:
- "*"
workflow_dispatch:
release:
types: [published, edited]
jobs:
build-and-publish-image:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
# Generate image tags based on semver
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/lllllllillllllillll/DweebUI
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
# Setup QEMU and Buildx for Multi-platform Support
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Only login to Registry if not running in a Pull Request
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build image and only publish if not a Pull Request
- name: Build and Publish Docker Image
uses: docker/build-push-action@v6
timeout-minutes: 30
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

5
.gitignore vendored Normal file
View file

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

145
CHANGELOG.md Normal file
View file

@ -0,0 +1,145 @@
## 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.
* Improved uninstall function and form id fix.
* WebUI Port can be changed in compose.yml
* Code clean-up.
* Updated dependencies (systeminformation).
## v0.06 (Nov 24th 2023)
* Multi-platform image (amd64/arm64).
* Removed Caddy from compose file.
* Proxy Manager UI can be enabled from environment variable.
* Removed hardcoded redis passwords.
* Repo change: Implemented image build-and-publish and dependabot (Thank you, gaby).
* Updated dependencies.
## v0.05 (Nov 17th 2023)
* Environment Variables and Labels are now unchecked by default.
* Support for Docker volumes.
* Fixed app uninstall.
* Fixed Proxy Manager.
* Updated functions to ignore the three DweebUI containers: DweebUI, DweebCache(redis), and DweebProxy(caddy).
* Visual updates: Tabs for networks, images, and volumes. Added 'update' option in container drop-down.
* Updated main.js to prevent javascript errors.
* Fix for templates using 'set' instead of 'default' in environment variables.
* Fixes for templates with no volumes or no labels.
* New README.md.
* New screenshots.
* Automatically persists data in docker volumes if there is no bind mount.
## v0.04 (Nov 11th 2023)
* Docker Image and Compose file available.
* The containers DweebUI and DweebCache are hidden from the dashboard.
* Default icon for containers.
* Fixed missing information in container details/edit modals (Ports, Env, Volumes, Labels).
## v0.03 (Nov 5th 2023)
* Container graphs now load instantly on refresh
* Working net data for server dashboard
* Redis is now installed as a docker container.
## v0.02 (Nov 1st 2023)
* Significant code clean-up and improvements
* CPU and RAM graphs for each container
* Updated Templates.json
* Fixed text color of VPN and VNC buttons
## v0.01 (Oct 15th 2023)
* First release. Not much working.

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY . /app
RUN npm install
EXPOSE 8000
CMD node server.js

135
README.md
View file

@ -1,40 +1,95 @@
# DweebUI
DweebUI is a simple docker web interface created using Node.js.
Pre-Pre-Pre-Pre-Pre Alpha v 0.01 ( :fire: Experimental. Don't install on any servers you care about :fire: )
* 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 :|
Requirements: Fresh Install of Debian 12.2
## Features
* Dashboard provides server metrics (cpu, ram, network, disk) and container controls on a single page.
* Partial Portainer Template Support (Network Mode, Ports, Volumes, Enviroment Variables, Labels, Commands, Restart Policy, Nvidia Hardware Acceleration).
* Light/Dark Mode.
* Support for multiple users is built in (but unused).
* Caddy Proxy Manager
* Pure Javascript. No Typescript. No Frameworks.
* User data is stored in a sqlite database and uses browser sessions and a redis store for authentication.
* Templates.json maintains compatability with Portainer, so you can use the template without needing to use DweebUI.
## Setup
* Download and extract DweebUI.zip to a fresh Debian 12.2 Install
```
cd DweebUI
chmod +x setup.sh
sudo ./setup.sh
```
Once setup is complete, I recommend installing Caddy first, then something like code-server.
The template is very rough.
## Credit
* UI was built using HTML and CSS elements from https://tabler.io/
* Apps template based on Portainer template provided by Lissy93 here: https://github.com/Lissy93/portainer-templates
* Most of the app icons were sourced from Walkxcode's dashboard icons here: https://github.com/walkxcode/dashboard-icons
<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] 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] Docker Compose Support.
* [ ] Update containers (planned).
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
* [ ] 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:
```
version: "3.9"
services:
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:
networks:
dweebui_net:
driver: bridge
```
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
Compose setup:
* 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)

180
app.js
View file

@ -1,180 +0,0 @@
const express = require("express");
const session = require("express-session");
const redis = require('connect-redis');
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats } = require('systeminformation');
const app = express();
const routes = require("./routes");
const PORT = 8000;
var Docker = require('dockerode');
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
const { dashCard } = require('./components/dashCard');
let DockerContainers, sent_list, clicked, open_ports, ServerMetrics, card_list, external_port, internal_port;
const redisClient = require('redis').createClient({
legacyMode:true
});
redisClient.connect().catch(console.log);
const RedisStore = redis(session);
const sessionMiddleware = session({
store:new RedisStore({client:redisClient}),
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.
}
})
app.set('view engine', 'ejs');
app.use([
express.static("public"),
express.json(),
express.urlencoded({ extended: true }),
sessionMiddleware,
routes
]);
const server = app.listen(PORT, async () => {
console.log(`App listening on port ${PORT}`);
});
const io = require('socket.io')(server);
io.engine.use(sessionMiddleware);
io.on('connection', (socket) => {
const user_session = socket.request.session;
// display client connection info
console.log(`${user_session.user} connected from ${socket.handshake.headers.host} ${socket.handshake.address} \n Active Sessions: ${io.engine.clientsCount}`);
// send list of running docker containers if sent_list contains data
if (sent_list != null) { socket.emit('cards', sent_list); }
// check if an install is in progress
if((app.locals.install != '') && (app.locals.install != null)){
socket.emit('install', app.locals.install);
}
// send server metrics to client
async function Metrics() {
Promise.all([currentLoad(), mem(), networkStats(), fsSize()]).then(([cpuUsage, ramUsage, netUsage, diskUsage]) => {
let cpu = Math.round(cpuUsage.currentLoad);
let ram = Math.round(((ramUsage.active / ramUsage.total) * 100));
let tx = netUsage[0].tx_bytes;
let rx = netUsage[0].rx_bytes;
let disk = diskUsage[0].use;
socket.emit('metrics', { cpu, ram, tx, rx, disk });
});
}
async function ContainersList() {
card_list = '';
open_ports = '';
external_port;
internal_port;
docker.listContainers({ all: true }, async function (err, data) {
for (const container of data) {
let imageVersion = container.Image.split('/');
let dockerService = imageVersion[imageVersion.length - 1].split(":")[0];
let containerId = docker.getContainer(container.Id);
let containerInfo = await containerId.inspect();
// console.log(containerInfo.Name.split('/')[1]);
// console.log(container.Image);
// console.log(containerInfo.HostConfig.RestartPolicy.Name);
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
console.log(`${value[0].HostPort}:${key}`);
external_port = value[0].HostPort;
internal_port = key;
}
// console.log('Volumes:');
// for (const [key, value] of Object.entries(containerInfo.Mounts)) {
// console.log(`${value.Source}: ${value.Destination}: ${value.RW}`);
// }
// console.log('Environment Variables:');
// for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
// console.log(`${key}: ${value}`);
// }
// console.log('Labels:');
// for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
// console.log(`${key}: ${value}`);
// }
// dockerContainerStats(container.Id).then((data) => {
// console.log(`${container.Names[0].slice(1)} // CPU: ${Math.round(data[0].cpuPercent)} // RAM: ${Math.round(data[0].memPercent)}`);
// });
let dockerCard = dashCard(container.Names[0].slice(1), dockerService, container.Id, container.State, container.Image, external_port, internal_port);
// open_ports += `-L ${external_port}:localhost:${external_port} `
card_list += dockerCard;
}
// emit card list is it's different from what was sent last time, then clear install local
if (sent_list !== card_list) {
sent_list = card_list;
app.locals.install = '';
socket.emit('cards', card_list);
console.log('Cards updated');
}
});
}
console.log('Starting Metrics');
ServerMetrics = setInterval(Metrics, 1000);
console.log('Starting Containers List');
DockerContainers = setInterval(ContainersList, 1000);
socket.on('clicked', (data) => {
// Prevent multiple clicks
if (clicked == true) { return; } clicked = true;
console.log(`${socket.request.session.user} wants to: ${data.action} ${data.container}`);
if (socket.request.session.role == 'admin') {
var containerName = docker.getContainer(data.container);
if ((data.action == 'start') && (data.state == 'stopped')) {
containerName.start();
} else if ((data.action == 'start') && (data.state == 'paused')) {
containerName.unpause();
} else if ((data.action == 'stop') && (data.state != 'stopped')) {
containerName.stop();
} else if ((data.action == 'pause') && (data.state == 'running')) {
containerName.pause();
} else if ((data.action == 'pause') && (data.state == 'paused')) {
containerName.unpause();
} else if (data.action == 'restart') {
containerName.restart();
}
} else {
console.log('User is not an admin');
}
clicked = false;
});
socket.on('disconnect', () => {
console.log('Stopping Metrics');
clearInterval(ServerMetrics);
console.log('Stopping Containers List');
clearInterval(DockerContainers);
});
});

View file

@ -1,978 +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" : "";
// 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;
}
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 '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.split(":")[0] ? volumes.bind.split(":")[0] : "";
let volume_container = volumes.container.split(":")[0] ? volumes.container.split(":")[0] : "";
let volume_readwrite = volumes.container.endsWith(":ro") ? "ro" : "rw";
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 = env ? "checked" : "";
let env_default = env.default ? env.default : "";
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 = label ? "checked" : "";
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="${data.image}"/>
</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="port_0_check" 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="port_1_check" 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="port_2_check" 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="port_3_check" 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="port_4_check" 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="port_5_check" 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="volume_0_check" 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="volume_1_check" 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="volume_2_check" 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="volume_3_check" 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="volume_4_check" 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="volume_5_check" 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="env_0_check" ${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="env_1_check" ${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="env_2_check" ${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="env_3_check" ${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="env_4_check" ${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="env_5_check" ${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="env_6_check" ${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="env_7_check" ${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="env_8_check" ${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="env_9_check" ${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="env_10_check" ${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="env_11_check" ${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="label_0_check" ${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="label_1_check" ${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="label_2_check" ${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="label_3_check" ${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="label_4_check" ${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="label_5_check" ${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="label_6_check" ${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="label_7_check" ${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="label_8_check" ${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="label_9_check" ${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="label_10_check" ${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="label_11_check" ${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="hwa_check" type="checkbox">
</div>
<div class="col">
<label class="form-label">Nvidia Hardware Acceleration</label>
<input type="text" class="form-control" name="command" value="Nvidia"/>
</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 };

27
compose.yaml Normal file
View file

@ -0,0 +1,27 @@
version: "3.9"
services:
dweebui:
container_name: dweebui
image: lllllllillllllillll/dweebui:v0.60
environment:
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
ports:
- 8000:8000
volumes:
- 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:
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,151 +0,0 @@
const { writeFileSync, mkdirSync, appendFileSync } = require("fs");
const { exec } = require("child_process");
const { dashCard } = require('../components/dashCard');
exports.Install = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = req.body;
let { port_0_check, port_1_check, port_2_check, port_3_check, port_4_check, port_5_check } = req.body;
let { volume_0_check, volume_1_check, volume_2_check, volume_3_check, volume_4_check, volume_5_check } = req.body;
let { env_0_check, env_1_check, env_2_check, env_3_check, env_4_check, env_5_check, env_6_check, env_7_check, env_8_check, env_9_check, env_10_check, env_11_check } = req.body;
let { label_0_check, label_1_check, label_2_check, label_3_check, label_4_check, label_5_check, label_6_check, label_7_check, label_8_check, label_9_check, label_10_check, label_11_check } = req.body;
let installCard = dashCard(req.body.name, req.body.service_name, '', 'installing', req.body.image, 0, 0);
req.app.locals.install = installCard;
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 ((port_0_check == 'on' || port_1_check == 'on' || port_2_check == 'on' || port_3_check == 'on' || port_4_check == 'on' || port_5_check == 'on') && (net_mode != 'host')) {
compose_file += `\n ports:`
for (let i = 0; i < 6; i++) {
if (req.body[`port_${i}_check`] == 'on') {
compose_file += `\n - ${req.body[`port_${i}_external`]}:${req.body[`port_${i}_internal`]}/${req.body[`port_${i}_protocol`]}`
}
}
}
// Volumes
if (volume_0_check == 'on' || volume_1_check == 'on' || volume_2_check == 'on' || volume_3_check == 'on' || volume_4_check == 'on' || volume_5_check == 'on') {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
if (req.body[`volume_${i}_check`] == 'on') {
compose_file += `\n - ${req.body[`volume_${i}_bind`]}:${req.body[`volume_${i}_container`]}:${req.body[`volume_${i}_readwrite`]}`
}
}
}
// Environment variables
if (env_0_check == 'on' || env_1_check == 'on' || env_2_check == 'on' || env_3_check == 'on' || env_4_check == 'on' || env_5_check == 'on' || env_6_check == 'on' || env_7_check == 'on' || env_8_check == 'on' || env_9_check == 'on' || env_10_check == 'on' || env_11_check == 'on') {
compose_file += `\n environment:`
}
for (let i = 0; i < 12; i++) {
if (req.body[`env_${i}_check`] == 'on') {
compose_file += `\n - ${req.body[`env_${i}_name`]}=${req.body[`env_${i}_default`]}`
}
}
// Add labels
if (label_0_check == 'on' || label_1_check == 'on' || label_2_check == 'on' || label_3_check == 'on' || label_4_check == 'on' || label_5_check == 'on' || label_6_check == 'on' || label_7_check == 'on' || label_8_check == 'on' || label_9_check == 'on' || label_10_check == 'on' || label_11_check == 'on') {
compose_file += `\n labels:`
}
for (let i = 0; i < 12; i++) {
if (req.body[`label_${i}_check`] == 'on') {
compose_file += `\n - ${req.body[`label_${i}_name`]}=${req.body[`label_${i}_value`]}`
}
}
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
if (env_0_check == 'on' || env_1_check == 'on' || env_2_check == 'on' || env_3_check == 'on' || env_4_check == 'on' || env_5_check == 'on' || env_6_check == 'on' || env_7_check == 'on' || env_8_check == 'on' || env_9_check == 'on' || env_10_check == 'on' || env_11_check == 'on') {
for (let i = 0; i < 12; i++) {
if (req.body[`env_${i}_check`] == 'on') {
if (req.body[`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]`
}
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
exec(`docker compose -f ./appdata/${name}/docker-compose.yml up -d`, (error, stdout, stderr) => {
if (error) { console.error(`error: ${error.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`stdout:\n${stdout}`);
});
} catch { console.log('error creating directory or compose file') }
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.Uninstall = async function (req, res) {
if (req.session.role == "admin") {
if (req.body.confirm == 'Yes') {
exec(`docker compose -f ./appdata/${req.body.service_name}/docker-compose.yml down`, (error, stdout, stderr) => {
if (error) { console.error(`error: ${error.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`stdout:\n${stdout}`);
});
}
// Redirect to the home page
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}

View file

@ -1,129 +1,587 @@
const User = require('../database/UserModel');
const { appCard } = require('../components/appCard')
const templates_json = require('../templates.json');
let templates = templates_json.templates;
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
import { parse } from 'yaml';
import multer from 'multer';
import AdmZip from 'adm-zip';
// sort templates alphabetically
templates = templates.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
});
const upload = multer({storage: multer.diskStorage({
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
filename: function (req, file, cb) { cb(null, file.originalname) },
})});
let alert = '';
let templates_global = '';
let json_templates = '';
let remove_button = '';
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);
// generate values for prev and next buttons so that i can go back and forth between pages
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>`
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;}
if (template_param == 'compose') {
let compose_files = readdirSync('templates/compose/');
app_count = compose_files.length;
last_page = Math.ceil(compose_files.length/28);
compose_files.forEach(file => {
if (file == '.gitignore') { return; }
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 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 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;
templates_global = templates;
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.processApps = async function(req, res) {
if (req.session.role == "admin") {
let search = req.body.search;
// Get the user.
let user = await User.findOne({ where: { UUID: req.session.UUID }});
let page = Number(req.params.page) || 1;
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 template_param = req.params.template || 'default';
// generate values for prev and next buttons so that i can go back and forth between pages
let prev = '/apps?page=' + (page - 1);
let next = '/apps?page=' + (page + 1);
if (page == 1) {
prev = '/apps?page=' + (page);
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 (page == last_page) {
next = '/apps?page=' + (page);
if (templates[i].categories[1]) {
cat_2 = (templates[i].categories[1]).toLowerCase();
}
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);
if (templates[i].categories[2]) {
cat_3 = (templates[i].categories[2]).toLowerCase();
}
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");
}
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';
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}`);
}
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
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: ""
});
}
}
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,22 +1,448 @@
const User = require('../database/UserModel');
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) => {
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 = '';
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;
}
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);
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 || '');
}
exports.Dashboard = async function (req, res) {
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] || '');
}
if (req.session.role == "admin") {
// 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] || '');
}
// get user data with matching UUID from sqlite database
let user = await User.findOne({ where: { UUID: req.session.UUID } });
// Render the home page
res.render("pages/dashboard", {
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.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;
}
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;
}
}
// 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: '',
});
}

View file

@ -1,60 +1,71 @@
const User = require('../database/UserModel');
const bcrypt = require('bcrypt');
import { User, Syslog } from '../database/models.js';
import bcrypt from 'bcrypt';
exports.Login = function(req,res){
export const Login = function(req,res){
if (req.session.user) { res.redirect("/logout"); }
else { res.render("login",{ "error":"", }); }
}
// 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
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.",
});
}
}
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
});
}
export const Logout = function(req,res){
req.session.destroy(() => {
res.redirect("/login");
});
}

View file

@ -1,6 +0,0 @@
exports.Logout = function(req,res){
// clear the session.
req.session.destroy();
// Redirect to the login page.
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);
}

View file

@ -1,85 +1,104 @@
const User = require('../database/UserModel');
const bcrypt = require('bcrypt');
import { User, Syslog, Permission } from '../database/models.js';
import bcrypt from 'bcrypt';
let SECRET = process.env.SECRET || "MrWiskers"
exports.Register = function(req,res){
// Check whether we have a session
export const Register = function(req,res){
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
} else {
// Render the signup page.
res.render("pages/register",{
res.render("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){
export const submitRegister = async function(req,res){
// Check if there is an existing user with that username.
let existingUser = await User.findOne({ where: {username:username}});
let { name, username, email, password, confirmPassword, secret } = req.body;
email = email.toLowerCase();
let adminUser = await User.findOne({ where: {role:"admin"}});
if(!existingUser){
// hash the password.
let hashedPassword = bcrypt.hashSync(password,10);
if (secret != SECRET) {
const syslog = await Syslog.create({
user: username,
email: email,
event: "Failed Registration",
message: "Invalid secret",
ip: req.socket.remoteAddress
});
}
if(!adminUser){
console.log('Creating admin User');
role = "admin";
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({
first_name: first_name,
last_name: last_name,
name: name,
username: username,
email: email,
password: hashedPassword,
role: role,
password: bcrypt.hashSync(password,10),
role: await userRole(),
group: 'all',
avatar: `<img src="./static/avatars/${avatar}">`
});
lastLogin: newLogin,
});
console.log(`Created: ${user.first_name}`);
// 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);
// 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",{
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.",
isLoggedIn:false
});
}
}else{
// return an error.
res.render("pages/register",{
"error":"User with that username already exists.",
isLoggedIn:false
});
}
}else{
} else {
// return an error.
res.render("register",{
"error":"User with that email already exists.",
});
}
} else {
// Redirect to the signup page.
res.render("pages/register",{
"error":"Please fill in all the fields and accept TOS.",
isLoggedIn:false
res.render("register",{
"error":"Please fill in all the fields.",
});
}
}

View file

@ -1,21 +1,10 @@
const User = require('../database/UserModel.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: '',
});
}

View file

@ -1,197 +0,0 @@
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
const { execSync } = require("child_process");
const { siteCard } = require('../components/siteCard');
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(`/home/docker/caddy/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
// format caddyfile
execSync(`docker exec caddy caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
if (stdout) { console.log(`stdout:\n${stdout}`); return; }
console.log(`Formatted ${domain}.Caddyfile`)
});
let site = siteCard(type, domain, host, port, 0);
// reload caddy config to enable new site
execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
if (stdout) { console.log(`stdout:\n${stdout}`); return; }
console.log(`reloaded caddy config`)
});
// append the site to site_list.ejs
appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
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)) {
console.log(`${key}: ${value}`);
execSync(`rm /home/docker/caddy/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`);
});
}
// reload caddy config to disable sites
try {
execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`reloaded caddy config`)
}); } catch (error) { console.log("No sites to reload") }
console.log('Removed Site(s)')
res.redirect("/refreshsites");
} else {
res.redirect("/");
}
}
exports.RefreshSites = async function (req, res) {
let domain, type, host, port;
let id = 1;
if (req.session.role == "admin") {
// Clear site_list.ejs
writeFileSync('./views/partials/site_list.ejs', '', function (err) {
if (err) {
console.log(err);
} else {
console.log('site_list.ejs has been cleared');
}
});
// check if /home/docker/caddy/sites/ contains any .json files, then delete them
try {
let files = readdirSync('/home/docker/caddy/sites/');
files.forEach(file => {
if (file.includes(".json")) {
execSync(`rm /home/docker/caddy/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('/home/docker/caddy/sites/');
sites.forEach(site_name => {
// convert the caddyfile of each site to json
execSync(`docker exec caddy caddy adapt --config /etc/caddy/sites/${site_name} --pretty >> /home/docker/caddy/sites/${site_name}.json`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`stdout:\n${stdout}`);
});
// read the json file
let site_file = readFileSync(`/home/docker/caddy/sites/${site_name}.json`, 'utf8');
// fix whitespace and parse the json file
site_file = site_file.replace(/ /g, " ");
site_file = JSON.parse(site_file);
// 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") }
// build the site card
let site = siteCard(type, domain, host, port, id);
// append the site card to site_list.ejs
appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
id++;
});
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/");
}
}
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");
}
}

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

2932
package-lock.json generated Normal file

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",
"connect-redis": "^6.1.3",
"dockerode": "^3.3.5",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"redis": "^4.6.5",
"sequelize": "^6.32.1",
"socket.io": "^4.6.1",
"sqlite3": "^5.1.6",
"systeminformation": "^5.17.12"
"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,128 +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');
//Update usage bars
socket.on('metrics', ({ cpu, ram, tx, rx, disk}) => {
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>`;
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
});
function drawCharts() {
var elements = document.querySelectorAll("#cardChart");
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: [37, 35, 44, 28, 36, 24, 65, 31, 37, 39, 62, 51, 35, 41, 35, 27, 93, 53, 61, 27, 54, 43, 4, 46, 39, 62, 51, 35, 41, 67]
}, {
name: "RAM",
data: [93, 54, 51, 24, 35, 35, 31, 67, 19, 43, 28, 36, 62, 61, 27, 39, 35, 41, 27, 35, 51, 46, 62, 37, 44, 53, 41, 65, 39, 37]
}],
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();
}
});
}
function buttonAction(button) {
// if the button name is 'CaddyProxyManager' and the value is 'install' grab the div with the id of 'sites' and remove d-none class. also add the d-none class to the div with the id of 'CaddyInstallCard'
if (button.name == 'CaddyProxyManager' && button.value == 'install') {
document.getElementById('sites').classList.remove('d-none');
document.getElementById('CaddyInstallCard').classList.add('d-none');
}
socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
}
socket.on('cards', (data) => {
console.log('cards deleted');
let deleteMeElements = document.querySelectorAll('.deleteme');
deleteMeElements.forEach((element) => {
element.parentNode.removeChild(element);
});
dockerCards.insertAdjacentHTML("afterend", data);
drawCharts();
});
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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

106
router/index.js Normal file
View file

@ -0,0 +1,106 @@
import express from "express";
import { Permission } from '../database/models.js';
export const router = express.Router();
// Controllers
import { Login, submitLogin, Logout } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, DashboardAction, Stats, Chart, SSE, UpdatePermissions } from "../controllers/dashboard.js";
import { Apps, appSearch, InstallModal, ImportModal, LearnMore, Upload, removeTemplate } from "../controllers/apps.js";
import { Users } from "../controllers/users.js";
import { Images } from "../controllers/images.js";
import { Networks, removeNetwork } from "../controllers/networks.js";
import { Volumes, addVolume, removeVolume } from "../controllers/volumes.js";
import { Account } from "../controllers/account.js";
import { Variables } from "../controllers/variables.js";
import { Settings } from "../controllers/settings.js";
import { Supporters, Thanks } from "../controllers/supporters.js";
import { Syslogs } from "../controllers/syslogs.js";
import { Install } from "../utils/install.js"
import { Uninstall } from "../utils/uninstall.js"
// Permission Middleware
const adminOnly = async (req, res, next) => {
if (req.session.role == 'admin') { next(); }
else { res.redirect('/dashboard'); }
}
const sessionCheck = async (req, res, next) => {
if (req.session.user) { next(); }
else { res.redirect('/login'); }
}
const permissionCheck = async (req, res, next) => {
if (req.session.role == 'admin') { next(); return; }
let user = req.session.user;
let action = req.path.split("/")[2];
let trigger = req.header('hx-trigger-name');
const userAction = ['start', 'stop', 'restart', 'pause', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
const userPaths = ['card', 'updates', 'hide', 'reset', 'alert'];
if (userAction.includes(action)) {
let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
if (permission) {
if (permission[action] == true) {
console.log(`User ${user} has permission to ${action} ${trigger}`);
next();
return;
}
else {
console.log(`User ${user} does not have permission to ${action} ${trigger}`);
}
}
} else if (userPaths.includes(action)) {
next();
return;
}
}
// Utils
router.post("/install", adminOnly, Install);
router.post("/uninstall", adminOnly, Uninstall);
// Routes
router.get("/login", Login);
router.post("/login", submitLogin);
router.get("/logout", Logout);
router.get("/register", Register);
router.post("/register", submitRegister);
router.get("/", sessionCheck, Dashboard);
router.get("/dashboard", sessionCheck, Dashboard);
router.post("/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
router.get("/sse", sessionCheck, SSE);
router.post("/updatePermissions", adminOnly, UpdatePermissions);
router.get("/stats", sessionCheck, Stats);
router.get("/chart", sessionCheck, Chart);
router.get("/images", adminOnly, Images);
router.post("/images/:action", adminOnly, Images);
router.get("/volumes", adminOnly, Volumes);
router.post("/addVolume", adminOnly, addVolume);
router.post("/removeVolume", adminOnly, removeVolume);
router.get("/networks", adminOnly, Networks);
router.post("/removeNetwork", adminOnly, removeNetwork);
router.get("/apps/:page?/:template?", adminOnly, Apps);
router.post("/apps", adminOnly, appSearch);
router.get("/remove_template/:template", adminOnly, removeTemplate);
router.get("/install_modal", adminOnly, InstallModal)
router.get("/import_modal", adminOnly, ImportModal)
router.get("/learn_more", adminOnly, LearnMore)
router.post("/upload", adminOnly, Upload);
router.get("/users", adminOnly, Users);
router.get("/syslogs", adminOnly, Syslogs);
router.get("/variables", adminOnly, Variables);
router.get("/settings", adminOnly, Settings);
router.get("/account", sessionCheck, Account);
router.get("/supporters", sessionCheck, Supporters);
router.post("/thank", sessionCheck, Thanks);

View file

@ -1,47 +0,0 @@
const express = require("express");
const router = express.Router();
const { Dashboard } = require("../controllers/dashboard");
const { AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/site_actions");
const { Install, Uninstall } = require("../controllers/app_actions");
const {Apps, processApps} = require("../controllers/apps");
const { Users } = require("../controllers/users");
const {Account} = require("../controllers/account");
const {Settings} = require("../controllers/settings");
const {Logout} = require("../controllers/logout");
const {Login, processLogin} = require("../controllers/login");
const {Register, processRegister} = require("../controllers/register");
router.get("/", Dashboard);
router.post("/install", Install)
router.post("/uninstall", Uninstall)
router.post("/addsite", AddSite)
router.post("/removesite", RemoveSite)
router.get("/refreshsites", RefreshSites)
router.post("/disablesite", DisableSite)
router.post("/enablesite", EnableSite)
router.get("/users", Users);
router.get("/apps", Apps);
router.post("/apps", processApps);
router.get("/settings", Settings);
router.get("/account", Account);
router.get("/login",Login); // Login page
router.post("/login",processLogin); // Process login
router.get("/register", Register); // Register page
router.post("/register",processRegister); // Process Register
router.get("/logout",Logout); // Logout
module.exports = router;

BIN
screenshots/apps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
screenshots/dashboard1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

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