Compare commits

..

412 Commits

Author SHA1 Message Date
Girish Ramakrishnan 0bb354bc4f mail: fix acl and perm issue with virtual All Mails 2023-08-22 10:31:48 +05:30
Girish Ramakrishnan 095bef8ca6 mail: namespace ordering broke usage reporting 2023-08-22 09:47:13 +05:30
Johannes Zellner 03529174de filemanager: also condense common buttons 2023-08-21 20:29:44 +02:00
Johannes Zellner 25d06690ec terminal: do not show labels for common buttons 2023-08-21 20:29:44 +02:00
Girish Ramakrishnan e833b859eb cloudron-setup: docker images are downloaded as part of installer now 2023-08-21 22:26:58 +05:30
Girish Ramakrishnan 4b6d4fe6be another take on prune images 2023-08-21 22:17:28 +05:30
Girish Ramakrishnan f152331615 Fix issue where backup config disappeared 2023-08-21 22:17:28 +05:30
Johannes Zellner c7ced6a487 dashboard: Remove verbose OpenID URLs 2023-08-21 18:09:47 +02:00
Girish Ramakrishnan 1ad94708b4 apps have to reconfigured in main thread
they cannot be done in the task process
2023-08-21 21:35:09 +05:30
Johannes Zellner 61047e374c terminal: wait for DOM to update the a-tag before opening it 2023-08-21 17:48:14 +02:00
Girish Ramakrishnan bf2531337f Fix crash on mail server change 2023-08-21 21:15:58 +05:30
Johannes Zellner be481ef006 frontend: update dependencies 2023-08-21 17:34:54 +02:00
Johannes Zellner 3bd5f9b027 filemanager: Use different owner map for apps and volumes 2023-08-21 17:34:40 +02:00
Johannes Zellner d05e16dc11 filemanager: Show uid if username is not known 2023-08-21 16:54:13 +02:00
Girish Ramakrishnan 91a4883b50 typo 2023-08-21 19:43:53 +05:30
Girish Ramakrishnan 79af6c1a68 On dashboard or email location change, reconfigure immediately 2023-08-21 18:34:07 +05:30
Girish Ramakrishnan 9e093db7d8 mailserver: fix crash when restarting 2023-08-21 15:19:42 +05:30
Girish Ramakrishnan 2427f15231 typo in branding route 2023-08-21 15:01:43 +05:30
Girish Ramakrishnan b895cc6aad capitalize progress 2023-08-21 14:40:57 +05:30
Johannes Zellner 40884705b4 Fixup demo note text 2023-08-17 13:45:07 +02:00
Johannes Zellner 98e43a6f5a Add login note for demo Cloudron 2023-08-17 13:38:47 +02:00
Girish Ramakrishnan 28bfab6700 LOCATION_TYPE can move into location.js 2023-08-17 16:05:19 +05:30
Girish Ramakrishnan 5c98b6f080 crash fixes 2023-08-17 13:02:36 +05:30
Girish Ramakrishnan 3d0ba557e5 add Location class 2023-08-17 10:44:07 +05:30
Girish Ramakrishnan de7879afb5 store subdomain in database instead of fqdn
this makes it more consistent with the locations table
2023-08-16 21:58:56 +05:30
Girish Ramakrishnan 1133a41b77 Fix proxy config not generated on restore 2023-08-16 12:52:52 +05:30
Girish Ramakrishnan e33ae8ae11 add missing export 2023-08-16 10:28:44 +05:30
Girish Ramakrishnan aa8c23c8b3 rework backup root
notes:
* backup root cannot come from backend. for dynamic mounts backend cannot know where it is mounted
* backupConfig is 3 parts - format / mount / password . there is also this rootPath (which should not be in db)
* password should be stored separately in settings at some point
* format has to be passed along everywhere because we allow restore from  same backupConfig but different format. we do this by saving the format in the backups table

fixes #819
2023-08-15 22:51:45 +05:30
Girish Ramakrishnan da49a69562 backups: testConfig is really testStorage 2023-08-15 19:59:00 +05:30
Girish Ramakrishnan 9dedf0ec05 validate the backup format 2023-08-15 19:57:51 +05:30
Girish Ramakrishnan cd9d49116e backups: move limits and storage into separate keys 2023-08-15 10:48:56 +05:30
Girish Ramakrishnan 630853abb5 move mountObjectFromBackupConfig into backups 2023-08-15 08:55:38 +05:30
Girish Ramakrishnan e6b85c2df7 remount does not need a backend hook 2023-08-15 08:55:38 +05:30
Girish Ramakrishnan d0fca9eeb9 trigger location changed only if activated 2023-08-14 14:20:20 +05:30
Girish Ramakrishnan 8cc08c734e Add to changes 2023-08-14 11:32:08 +05:30
Girish Ramakrishnan 4b1b38be63 make tests work again 2023-08-14 11:08:38 +05:30
Girish Ramakrishnan 4acbb7136a proper task name for dashboard change 2023-08-14 10:45:12 +05:30
Girish Ramakrishnan abff970169 make use of fqdn function 2023-08-14 09:35:08 +05:30
Girish Ramakrishnan 2b53ea0260 Fix dashboard config not getting generated 2023-08-14 02:08:10 +05:30
Girish Ramakrishnan a7be30a816 better naming of the dashboard functions 2023-08-13 10:38:07 +05:30
Girish Ramakrishnan e723c3c19b move dashboard change routes under dashboard/ 2023-08-13 10:06:01 +05:30
Girish Ramakrishnan 7b32cb16f3 move platform status into services 2023-08-12 22:29:09 +05:30
Girish Ramakrishnan 68a3c267e5 move config route under dashboard
it's essentially giving info for various parts of the ui
2023-08-12 22:25:49 +05:30
Girish Ramakrishnan 070f6e5de3 move startup logic to platform.js 2023-08-12 22:25:46 +05:30
Girish Ramakrishnan 559125cd3c remove unused require 2023-08-12 18:02:55 +05:30
Girish Ramakrishnan c62091b077 system: getUbuntuVersion 2023-08-11 21:47:49 +05:30
Girish Ramakrishnan f71e622fdb keep dropdown alphabetical 2023-08-11 21:09:36 +05:30
Girish Ramakrishnan eee49a8291 move dashboard setting into dashboard.js 2023-08-11 21:04:10 +05:30
Girish Ramakrishnan 27ce8f9351 oidc: fix crash when rendering error 2023-08-11 18:38:03 +05:30
Johannes Zellner cacf0d34f5 Add oidc views footer 2023-08-11 13:53:23 +02:00
Johannes Zellner 34f2386a9d dashboard: merge main.js into index.js 2023-08-11 12:25:40 +02:00
Johannes Zellner 4936475c2a Merge oidc settings for user directory view 2023-08-11 11:32:45 +02:00
Girish Ramakrishnan cd0b51dac2 Do not continue processing after redirect 2023-08-11 11:43:26 +05:30
Girish Ramakrishnan 1041b3b8ab plural 2023-08-11 07:35:57 +05:30
Girish Ramakrishnan 955a43723f cleanup status route
this is now purely a healthcheck route and nothing else

at some point, we will server render password reset and setup account views
2023-08-10 22:29:48 +05:30
Girish Ramakrishnan 1cdd528b45 separate the provision status and cloudron status 2023-08-10 22:29:47 +05:30
Johannes Zellner 98719aa942 Remove unused includes in oidc views 2023-08-10 17:06:00 +02:00
Girish Ramakrishnan 57772662aa move provisioning routes into /provision/ 2023-08-10 16:52:10 +05:30
Girish Ramakrishnan 6c4aa605df move various login routes under auth/ 2023-08-10 16:24:10 +05:30
Girish Ramakrishnan 9ba6908764 use list pattern when listing 2023-08-10 16:21:12 +05:30
Johannes Zellner d3b58483bd Update translations 2023-08-10 00:09:24 +02:00
Johannes Zellner 63ed900087 Purge user settings from settings view elements 2023-08-10 00:05:56 +02:00
Johannes Zellner b5ab7851c1 Remove user directory settings and oidc from users view 2023-08-09 23:53:36 +02:00
Johannes Zellner 4de2a477c6 Remove user directory from users view 2023-08-09 23:42:45 +02:00
Johannes Zellner 094fdad9a7 Remove externalldap from users view 2023-08-09 23:39:54 +02:00
Johannes Zellner 6eefe4c7c9 Duplicate users view into user settings view 2023-08-09 23:38:43 +02:00
Johannes Zellner 621ffb404c Remove unused subscription modals 2023-08-09 23:36:29 +02:00
Johannes Zellner 527c2f0baf Remove unused status api properties and label others 2023-08-09 17:48:03 +02:00
Johannes Zellner 842d7e6b61 Add block device selector in restore view 2023-08-09 12:14:37 +02:00
Johannes Zellner fb4921e2d3 Do not ignore mount failures on restore 2023-08-08 20:52:32 +02:00
Girish Ramakrishnan e6c43c84e4 hardcode yellowtent user uid
when we use an external disk, we chown 777 the mountpoint so that the
yellowtent user can write to it. the files are created as the 'yellowtent'
user.

when this disk is attached to another server for a restore, the new server's
yellowtent user may not be able to access the files if the uid does not match
between the old and new server.

for this, reason hardcode the uid
2023-08-08 23:18:43 +05:30
Johannes Zellner 8777a60b99 Make disk backup config known in restore view 2023-08-08 18:36:55 +02:00
Girish Ramakrishnan c6db1c70c0 docker: fix image prune
it seems docker images --digests cloudron/sftp --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}
broke at some point
2023-08-08 21:21:00 +05:30
Johannes Zellner 7d9e697d85 dashboard: remove some debug console.logs() 2023-08-08 15:52:09 +02:00
Johannes Zellner 10646e9e04 Add generic disk (partition) backup provider to replace ext4 and xfs 2023-08-08 15:11:22 +02:00
Johannes Zellner 5ef8d8d3b0 Add uuid to block device listing 2023-08-08 12:34:19 +02:00
Johannes Zellner e9f3f13564 Only always use token types from tokens.js 2023-08-07 19:26:04 +02:00
Girish Ramakrishnan 8f20a09791 Fix update route crash 2023-08-05 08:48:03 +05:30
Girish Ramakrishnan 67ee82abb9 remove settings.dashboardOrigin 2023-08-04 22:10:14 +05:30
Girish Ramakrishnan 4cdf37b060 settings: move mailFqdn/Domain into mailServer 2023-08-04 22:02:24 +05:30
Girish Ramakrishnan 946e5caacb split mail and mailserver
mail = all the per-domain code
mailserver = all the mail server level code
2023-08-04 20:54:39 +05:30
Girish Ramakrishnan fb9d8c23e1 move appstore urls into appstore.js 2023-08-04 15:41:41 +05:30
Girish Ramakrishnan 37ae142a16 keep the cloudron routes close 2023-08-04 14:17:13 +05:30
Girish Ramakrishnan 6aad89ae6e demo is just a constant, not a setting 2023-08-04 14:13:30 +05:30
Girish Ramakrishnan d79d24efad remove settings route entirely, redundant by now 2023-08-04 14:03:04 +05:30
Girish Ramakrishnan 2cdbf4d2c5 move server routes into /system 2023-08-04 13:42:21 +05:30
Girish Ramakrishnan 1264cd1dd7 reverseproxy: move renew and trusted ip routes 2023-08-04 13:19:48 +05:30
Girish Ramakrishnan a49cb0b080 move sync_dns out of cloudron route into domains 2023-08-04 12:55:57 +05:30
Girish Ramakrishnan a4c3d39cc3 Fix eventlog route 2023-08-04 12:46:54 +05:30
Girish Ramakrishnan da73067315 rename change notifiers to have handle prefix 2023-08-04 11:54:15 +05:30
Girish Ramakrishnan e73b75e4b5 settings: move backup settings 2023-08-04 11:54:12 +05:30
Girish Ramakrishnan 77c66d9a02 settings: move provider to provision 2023-08-04 11:01:45 +05:30
Girish Ramakrishnan 775246946a settings: move language and tz into cloudron.js 2023-08-04 10:58:04 +05:30
Girish Ramakrishnan ec23c7d2b8 Suppress aws sdk warning
https://github.com/aws/aws-sdk-js/issues/4354#issuecomment-1664694545
2023-08-04 09:21:48 +05:30
Girish Ramakrishnan 5603b9e811 move updater routes and settings under /api/v1/updater 2023-08-03 15:35:27 +05:30
Johannes Zellner db26a6beb9 dashboard: only show volumes UI for admins and owners 2023-08-03 10:43:28 +02:00
Girish Ramakrishnan 47d57a3971 fold sysinfo into network
the backends are network backends
2023-08-03 13:38:42 +05:30
Girish Ramakrishnan a4d57e7b08 refactor into getServiceConfig 2023-08-03 12:52:47 +05:30
Girish Ramakrishnan bbc6ba1a35 settings: move service setting into services.js
this also introduces getJson/setJson
2023-08-03 11:50:00 +05:30
Girish Ramakrishnan 3caf0c3902 Fix crash in getConfig 2023-08-03 09:03:47 +05:30
Girish Ramakrishnan d12e6ee2b3 settings: make user_directory setting route 2023-08-03 08:29:12 +05:30
Girish Ramakrishnan d475df8d63 settings: rename to directory_server_config 2023-08-03 07:35:14 +05:30
Girish Ramakrishnan 92a103d635 settings: move ipv6/ipv4 config into network
this also rename sysinfo_config to ipv4_config
2023-08-03 06:40:04 +05:30
Girish Ramakrishnan f2e56cbdd8 Fix crash on startup 2023-08-03 06:39:35 +05:30
Girish Ramakrishnan c97441f7d9 settings: remove cookie secret default 2023-08-03 02:48:24 +05:30
Girish Ramakrishnan 67e4c90d37 settings: move directory server config to it's own route 2023-08-03 02:48:21 +05:30
Girish Ramakrishnan 4a34c390f8 settings: move externaldap setting 2023-08-03 02:43:26 +05:30
Girish Ramakrishnan a19e502198 settings: move dynamic dns to network
and add tests
2023-08-02 23:02:40 +05:30
Girish Ramakrishnan fccc2d04a9 settings: move support config to support 2023-08-02 23:02:40 +05:30
Girish Ramakrishnan eb4213d61d settings: cloudronId is only ever set
we use subscription API to get the cloudronId, never from database
2023-08-02 23:02:40 +05:30
Girish Ramakrishnan e0d07c3c19 settings: move branding settings into branding.js 2023-08-02 23:02:40 +05:30
Girish Ramakrishnan 85a73af303 settings: remove appstore listing config
this is not used anymore
2023-08-02 23:02:40 +05:30
Girish Ramakrishnan be4c3575fb settings: move web/api token to appstore 2023-08-02 23:02:40 +05:30
Girish Ramakrishnan e1fd369c6d settings: move cookie secret into oidc 2023-08-02 23:02:40 +05:30
Girish Ramakrishnan 77e6b69a63 settings: remove unstable apps key
it's not used anymore
2023-08-02 23:02:40 +05:30
Girish Ramakrishnan c7f2a04e8c settings: move reverse proxy config 2023-08-02 23:02:39 +05:30
Girish Ramakrishnan c4a8255fdd settings: move firewall config to network 2023-08-02 23:02:39 +05:30
Girish Ramakrishnan 8fe992318e settings: move trusted ip setting to reverseproxy 2023-08-02 23:02:39 +05:30
Johannes Zellner f2317c2a81 show filemanager button in app mounts section 2023-08-02 13:33:40 +02:00
Girish Ramakrishnan 516dd89d92 settings: list already applies default logic 2023-08-02 15:35:05 +05:30
Girish Ramakrishnan 68b4bf1667 backupformat: print the backupFilePath 2023-08-02 09:50:34 +05:30
Johannes Zellner 30880de82f filemanager: close viewer on esc 2023-08-01 18:45:24 +02:00
Girish Ramakrishnan ee836e6646 mail: 'my' location is available as mail location
move the reserve domains check to app location validation code
2023-08-01 19:33:59 +05:30
Girish Ramakrishnan 7d929aca54 rsync: fix crash 2023-08-01 19:03:24 +05:30
Girish Ramakrishnan e65c1fb718 graphs: show old backup size and location if > 1GB 2023-08-01 18:44:27 +05:30
Girish Ramakrishnan 0722692210 graphs: always show /var/backups size
often this has old backups
2023-08-01 17:38:48 +05:30
Johannes Zellner 28dab0bc9b dashboard: add separator between disks 2023-08-01 14:01:57 +02:00
Girish Ramakrishnan 54e33a0ece graphs: no disk speed for network disks 2023-08-01 17:17:10 +05:30
Girish Ramakrishnan 80bf8e3ffe Update packages 2023-08-01 11:42:58 +05:30
Johannes Zellner 8e10477170 Add direcotry server tests for member and uniquemember attributes 2023-07-31 13:19:42 +02:00
Johannes Zellner 650966a7e5 directoryserver: Add member and uniquemember attributes
https://datatracker.ietf.org/doc/html/rfc4519#section-2.17
https://datatracker.ietf.org/doc/html/rfc4519#section-2.40
2023-07-31 13:13:07 +02:00
Johannes Zellner 65769e5701 ldap uses lower-case attributes 2023-07-31 13:12:39 +02:00
Johannes Zellner 7099102a79 filemanager: do not rely on history when closing viewers 2023-07-31 11:31:27 +02:00
Girish Ramakrishnan 740e69c8dd change redirections to 301 2023-07-31 06:04:49 +05:30
Johannes Zellner 72ccac2753 frontend: update pankow for dragndrop fixes 2023-07-30 19:43:31 +02:00
Johannes Zellner ae5748ffd1 frontend: update pankow 2023-07-30 13:53:52 +02:00
Girish Ramakrishnan 4a522ce99b cloudflare: key type selector should be first 2023-07-30 15:53:47 +05:30
Johannes Zellner b3916622e8 filemanager: bring some drag'n'drop functionality via pankow 2023-07-28 19:31:53 +02:00
Johannes Zellner 56e1f53890 Fix oidc tests after removing logoutRedirectUri 2023-07-28 16:47:10 +02:00
Girish Ramakrishnan 1f4c71dcd6 tests: configure apps needs an array 2023-07-28 14:46:31 +05:30
Girish Ramakrishnan 0ab4bc543f Fix backup.download tests 2023-07-28 13:15:08 +05:30
Girish Ramakrishnan 99bc30ad07 Update packages 2023-07-28 09:36:02 +05:30
Girish Ramakrishnan ab67c04f27 mail: add virtual All Mail mailbox 2023-07-27 22:56:36 +05:30
Girish Ramakrishnan 041faa10d9 turn: fix config for file logging and auth 2023-07-27 17:11:55 +05:30
Johannes Zellner f67fd2bc79 dashboard: Show service memory usage percent 2023-07-27 10:46:41 +02:00
Johannes Zellner 2a7b320834 logviewer: remove extra gap in top buttons 2023-07-26 19:49:44 +02:00
Johannes Zellner 348012823b More filemanger addon fixes 2023-07-26 16:41:16 +02:00
Johannes Zellner a4e2ed2253 New sftp addon to fix permission issue when files get overwritten 2023-07-26 14:36:21 +02:00
Johannes Zellner 3eedbdd163 logviewer: fix button margins for non-app types 2023-07-26 12:53:15 +02:00
Johannes Zellner bdc07bbbc7 frontend: update dependencies bringing in list view sorting 2023-07-25 16:42:13 +02:00
Girish Ramakrishnan d9a9ae2add oidc: log which app the user logged into 2023-07-25 18:40:48 +05:30
Girish Ramakrishnan b533e5273d oidc: set authType to oidc 2023-07-25 18:40:48 +05:30
Johannes Zellner e13d905f32 Store OpenID cookie secret in settings db and make it unique per instance 2023-07-25 12:40:05 +02:00
Girish Ramakrishnan be24ed64f8 lint 2023-07-25 13:21:41 +05:30
Girish Ramakrishnan ecc4d58bb2 oidc: comment out some debugs 2023-07-25 12:31:05 +05:30
Girish Ramakrishnan 9a359a27f5 backups: download is now async 2023-07-25 10:33:03 +05:30
Girish Ramakrishnan 2bec56145e add to changes 2023-07-25 10:33:03 +05:30
Johannes Zellner e97747762e Raise login event 2023-07-24 20:49:58 +02:00
Girish Ramakrishnan 3d5c21d9ca backups: encrypted backups must have .enc extension 2023-07-24 22:25:06 +05:30
Girish Ramakrishnan febac9e8ca backups: put the dashboard domain in the backup config 2023-07-24 21:31:02 +05:30
Johannes Zellner c3574614bc filemanager: make footer render the custom branding 2023-07-24 12:07:23 +02:00
Johannes Zellner fcfc8ce66d frontend: update readme 2023-07-24 10:32:43 +02:00
Johannes Zellner 4c185fb3b4 Reconfigure apps on dashboard domain change, if they use oidc addon 2023-07-21 20:02:35 +02:00
Johannes Zellner 00b5438ec5 oidc: explicitly disable rpInitiatedLogout 2023-07-20 16:43:58 +02:00
Johannes Zellner d361962d5c dashboard: fixup pencil icons in oidc view 2023-07-20 13:40:39 +02:00
Johannes Zellner 5489285406 oidc: remove now unsupported provider logout handling 2023-07-20 13:26:07 +02:00
Johannes Zellner be4b93ea2a namecheap: ensure we don't fail if no dns records exist 2023-07-19 14:51:42 +02:00
Johannes Zellner bd2e51ba1b frontend: update pankow dependency 2023-07-19 11:48:35 +02:00
Johannes Zellner 18c54aa8c6 logviewer: hide some buttons on mobile to avoid overflow 2023-07-19 11:45:34 +02:00
Johannes Zellner 3a3972822e Update translations 2023-07-18 18:56:38 +02:00
Johannes Zellner dd750d5d68 Remove old filemanager assets 2023-07-18 18:55:44 +02:00
Johannes Zellner 978faa1f68 terminal: support ctrl+shift+c/v for copy paste 2023-07-18 18:05:07 +02:00
Johannes Zellner 024a9c6e2b Remove old logs viewer 2023-07-18 17:44:22 +02:00
Johannes Zellner ac33570645 Remove old terminal 2023-07-18 17:26:13 +02:00
Johannes Zellner 9399b430d6 terminal: remove unused placeholder element 2023-07-18 17:18:19 +02:00
Johannes Zellner 1affadad8e Use vuejs based terminal in all places 2023-07-18 12:39:18 +02:00
Johannes Zellner f2c511902c fatalError needs to be a boolean false for the dialog widget 2023-07-17 19:37:07 +02:00
Johannes Zellner 6940de7465 terminal: show fatal error for invalid appid 2023-07-17 19:33:22 +02:00
Girish Ramakrishnan 9b872bbbd6 add hyphen in notfound 2023-07-17 09:59:29 +05:30
Girish Ramakrishnan 7a71c86bd8 cloudron-setup: validate setup token upfront
this allows use to re-run setup
2023-07-16 10:33:31 +05:30
Girish Ramakrishnan 2e20d757b1 cloudron-setup: validate the setup token 2023-07-16 10:01:47 +05:30
Girish Ramakrishnan 050a82039a getBackupProviderStatus -> getProviderStatus 2023-07-15 11:00:45 +05:30
Johannes Zellner 159ff1704f Always use full origin for api origin 2023-07-14 18:45:25 +02:00
Johannes Zellner be16ad6953 Terminal: add download file dialog 2023-07-14 18:18:55 +02:00
Johannes Zellner c1b393d926 Terminal: add file upload to /tmp 2023-07-14 17:32:56 +02:00
Johannes Zellner 1f4827f5c5 terminal: improve topbar button layout 2023-07-14 16:53:12 +02:00
Johannes Zellner b239e81065 terminal: support cron/scheduler 2023-07-14 16:39:27 +02:00
Johannes Zellner ee2cd0b573 Give success buttons our color scheme 2023-07-14 16:39:12 +02:00
Johannes Zellner c3d4769956 terminal: support addon injection 2023-07-14 16:06:03 +02:00
Johannes Zellner 698a5be41a frontend: update dependencies 2023-07-14 15:44:46 +02:00
Johannes Zellner d162ffe508 First version of vuejs terminal 2023-07-14 14:48:58 +02:00
Girish Ramakrishnan 6bf7a1a2d8 Add missing ISTATE 2023-07-14 18:09:07 +05:30
Girish Ramakrishnan 1d69207e6e redis: do not list in services when disabled 2023-07-14 18:01:21 +05:30
Girish Ramakrishnan 754cb17254 Update translations 2023-07-14 17:44:03 +05:30
Girish Ramakrishnan e1ff5f1cae ui: optional redis
fixes #810
2023-07-14 12:43:32 +05:30
Girish Ramakrishnan 866cf75012 add a TODO 2023-07-14 08:34:05 +05:30
Johannes Zellner 4c24de53e4 Some layout fixes for the apps service tab 2023-07-13 17:15:10 +02:00
Johannes Zellner d75c8e2858 various filemanager and logs improvements 2023-07-13 15:37:27 +02:00
Girish Ramakrishnan 25328d884f redis: make optional
part of #810
2023-07-13 16:46:09 +05:30
Girish Ramakrishnan f34840e1a3 mail: use the new services change task type 2023-07-13 16:46:09 +05:30
Johannes Zellner 4cb017e0e1 logs: fix page title and favicon 2023-07-13 12:14:37 +02:00
Girish Ramakrishnan 519b258a25 make turn service optional
part of #810
2023-07-13 15:32:28 +05:30
Girish Ramakrishnan a2c53df042 typo 2023-07-13 12:49:58 +05:30
Girish Ramakrishnan a28ca8fed2 backups: Clean cache if anything other than limits changes 2023-07-13 12:46:42 +05:30
Girish Ramakrishnan 68e56f903d validate encryption password separately 2023-07-13 12:42:38 +05:30
Girish Ramakrishnan 95314d46e2 backup policy must be inserted 2023-07-13 12:27:44 +05:30
Girish Ramakrishnan c86059e070 backups: move limits into a sub object
fixes #817
2023-07-13 12:17:57 +05:30
Girish Ramakrishnan 1a5cbfb2a1 delete spurious mountStatus while we are at it 2023-07-13 11:10:40 +05:30
Girish Ramakrishnan 9cebde3005 backups: split config and policy
keeping them together makes the test/validation quite complicated.
for example, when policy is changed, we test the storage backends

part of #817
2023-07-13 11:07:06 +05:30
Girish Ramakrishnan 7926ff2811 test: only suppress starttask.sh output and not sudo
the remote support logic uses sudo output in tests
2023-07-13 09:13:28 +05:30
Girish Ramakrishnan 13a8926f60 sudo: suppress starttask.sh logs in test 2023-07-13 09:01:14 +05:30
Johannes Zellner 8aec0f52ba logs: some style improvments 2023-07-12 20:45:15 +02:00
Johannes Zellner 0ccbc76f31 logs: fix logline hover background for timestamp 2023-07-12 14:40:35 +02:00
Johannes Zellner 76fa45c88d logs: Remove unused import 2023-07-12 14:39:26 +02:00
Johannes Zellner 1d4a680851 Fix focus state on p-buttons 2023-07-12 14:37:50 +02:00
Johannes Zellner e9f6a163d9 Use new logsviewer 2023-07-12 14:33:57 +02:00
Johannes Zellner caa160b3fd Move filemanager/ to frontend/ 2023-07-12 14:22:58 +02:00
Johannes Zellner 9b6957b52f logs: fix autoscrolling 2023-07-12 14:16:48 +02:00
Johannes Zellner f48b04ca87 Reimplement the logsviewer in primevue 2023-07-12 13:56:10 +02:00
Girish Ramakrishnan 0ab72f5900 appdata: cannot use cifs or sshfs
Fixes #827
2023-07-11 21:37:26 +05:30
Johannes Zellner 1bf91413c4 filemanager: improve prev/next in image viewer 2023-07-11 17:27:33 +02:00
Johannes Zellner c25521cded filemanager: prevent page from reload during deletion operation 2023-07-11 15:39:08 +02:00
Johannes Zellner 783c6c20c1 filemanager: ellipse string when deleting many files 2023-07-11 15:29:14 +02:00
Girish Ramakrishnan 5beb7d7d92 Fix tests 2023-07-11 18:45:49 +05:30
Johannes Zellner 2d4e7c9c0a filemanager: Do not reload whole state on folder change 2023-07-11 15:02:37 +02:00
Johannes Zellner 39498616a6 native eventhandler can't use this 2023-07-11 15:02:37 +02:00
Johannes Zellner da4c4f5530 Update translations 2023-07-11 15:02:37 +02:00
Girish Ramakrishnan b56a7f854c remotesupport: remove superfluous sshd_config check 2023-07-11 18:09:40 +05:30
Johannes Zellner d22680bc86 filemanager: support prev/next in image viewer 2023-07-11 13:37:03 +02:00
Johannes Zellner aa00742093 filemanager: give pasting busy indicator and prevent tab closing 2023-07-11 12:38:35 +02:00
Johannes Zellner 63c5aa1984 If we have a file conflict append -copy until we don't 2023-07-11 12:26:06 +02:00
Girish Ramakrishnan 13e4093d05 test: mysql 8.0 2023-07-11 15:37:31 +05:30
Girish Ramakrishnan 4c422e48b2 check-install: print all sudo instructions at once 2023-07-11 14:58:09 +05:30
Girish Ramakrishnan 249e6ffa2c redis: add no auth warning 2023-07-11 14:19:24 +05:30
Johannes Zellner 8eadce1201 Update pankow to have some page up/down support 2023-07-10 20:35:08 +02:00
Girish Ramakrishnan b8c14b1d7f Fix translations 2023-07-10 23:23:25 +05:30
Girish Ramakrishnan e410844350 mail: validate the mail server hostname 2023-07-10 23:05:17 +05:30
Girish Ramakrishnan 0049e269d3 email: move server location to it's own card
comples #826
2023-07-10 22:29:49 +05:30
Johannes Zellner 287ad9034d filemanager: update dependencies to support selectAll 2023-07-10 17:10:29 +02:00
Girish Ramakrishnan f8ec24b973 lint 2023-07-10 20:32:53 +05:30
Johannes Zellner 2cfa5511d5 Do not require applink icons to be pngs 2023-07-10 15:56:59 +02:00
Johannes Zellner 25abd8a67d Support more favicon cases for applinks 2023-07-10 15:22:56 +02:00
Johannes Zellner 3a5d570e3c Do not update applink icon if it is not set in update 2023-07-10 14:21:06 +02:00
Girish Ramakrishnan df54ba3a0a Add AVX check in preparation for mongodb 5 2023-07-09 12:54:12 +05:30
Girish Ramakrishnan 78877f3731 Show upgrade fail message that ubuntu 18.04 is now required 2023-07-09 12:53:59 +05:30
Girish Ramakrishnan d9d38ae402 dyndns: keep going if one or more domains fail to update 2023-07-09 08:09:36 +05:30
Girish Ramakrishnan 23f0eba1bd dyndns: run as a task
this lets us display logs
2023-07-08 21:21:06 +05:30
Girish Ramakrishnan 56b7cc4041 better error message when deleting domain
Fixes #815
2023-07-08 17:48:07 +05:30
Girish Ramakrishnan 07457703b1 mail: consistently use disk size to calculate usage
In the mail overivew page, we use disk size
In the per mailbox page, we use quota size
2023-07-08 09:56:56 +05:30
Johannes Zellner 5fc0a5f9a2 filemanager: Fix BASE_URL of fallback icon when deployed 2023-07-07 17:39:43 +02:00
Johannes Zellner c0b2d61583 Give oidc login button a id for easier testing 2023-07-07 10:45:55 +02:00
Johannes Zellner d74993f6ac Use sftp 3.7.3 to fix symlink deletion 2023-07-07 10:38:17 +02:00
Girish Ramakrishnan a651aa44f4 7.5.1 changes 2023-07-07 08:22:21 +05:30
Girish Ramakrishnan cf63261760 mail: fix issue where mail usage were reported incorrectly 2023-07-07 08:15:26 +05:30
Johannes Zellner e16eba7c66 Do not use translation templates in JS due to escaping issues 2023-07-06 19:01:39 +02:00
Johannes Zellner 736829445c Remove dead code 2023-07-05 11:25:09 +02:00
Girish Ramakrishnan 20856c9ee8 Remove obsolete development section
we are now a mono repo. dashboard and hotfix tools are here.
2023-07-05 13:44:53 +05:30
Johannes Zellner f1c6130cbd Fixup linter error 2023-07-04 16:23:59 +02:00
Johannes Zellner 7443847697 Use branding cloudron name for oidc login 2023-07-04 16:23:48 +02:00
Johannes Zellner 0294859839 dashboard: only selectively apply text-stroke 2023-07-02 17:49:40 +02:00
Johannes Zellner ccb925be5d dashboard: use text-stroke instead of drop-shadow to avoid z-index breakage 2023-07-02 12:31:43 +02:00
Girish Ramakrishnan 7835533838 typo 2023-07-01 13:34:58 +05:30
Girish Ramakrishnan 779997e7fc 7.4.3 changelog
(cherry picked from commit a08ac8de1b)
2023-07-01 13:10:51 +05:30
Girish Ramakrishnan b0e2129e2f add today's release file 2023-07-01 13:08:00 +05:30
Girish Ramakrishnan f9478d1e76 postgresql: add fix for taiga 2023-06-30 22:06:23 +05:30
Girish Ramakrishnan ab2056138e Give more time to resolve 2023-06-30 19:10:23 +05:30
Girish Ramakrishnan 5f0bcf62dd dig: use built-in resolver timeout 2023-06-30 19:09:19 +05:30
Johannes Zellner 94e2ce2968 filemanager: some fixes from the pankow module 2023-06-30 15:17:43 +02:00
Girish Ramakrishnan aea58a2b76 lint 2023-06-30 18:27:18 +05:30
Johannes Zellner 5433552710 filemanager: allow pasting on non-folders to cwd 2023-06-30 14:14:51 +02:00
Johannes Zellner d2b39351b8 Clear the correct mail status notification 2023-06-29 11:35:07 +02:00
Johannes Zellner a3649ea039 filemanager: placeholder for dark theme 2023-06-26 17:55:48 +02:00
Johannes Zellner f7ca78a8a6 filemanager: Only init vue app after we fetch language files to avoid UI shaking 2023-06-26 16:35:31 +02:00
Girish Ramakrishnan 853677ab2e appstore: fix crash because of error.message access 2023-06-26 18:06:37 +05:30
Johannes Zellner 7aae3790a7 oidc: Do not support logout 2023-06-26 13:02:57 +02:00
Girish Ramakrishnan 4cd54f1026 release: make changelog case insensitive 2023-06-25 19:19:23 +05:30
Girish Ramakrishnan 0eb32b8a58 Update CHANGES 2023-06-25 16:36:55 +05:30
Girish Ramakrishnan 37e3278f23 Update mail container for haraka fixes 2023-06-25 15:52:52 +05:30
Johannes Zellner 7cee40b491 filemanager: Remove back/goup button 2023-06-22 18:56:52 +02:00
Johannes Zellner fae23bd4fc filemanager: update pankow 2023-06-22 18:12:11 +02:00
Johannes Zellner 148a189bb2 filemanager: further fix the current folder entry 2023-06-22 18:11:05 +02:00
Johannes Zellner c3778f94c4 filemanager: set correct name for activeDirectory 2023-06-22 15:51:24 +02:00
Johannes Zellner b7fbffcb42 various filemanager fixes 2023-06-22 15:20:54 +02:00
Girish Ramakrishnan 6259849958 apphealth: timeout is already in msecs 2023-06-22 18:24:59 +05:30
Johannes Zellner eb767bb3b1 filemanager: add missing colon for props 2023-06-22 13:23:43 +02:00
Johannes Zellner a6f01b2455 Ensure all filemanager buttons explicitly use Noto font 2023-06-22 12:57:04 +02:00
Johannes Zellner 4fe055c3a8 oidc: automatically submit consent form
Fixes #828
2023-06-21 13:14:45 +02:00
Girish Ramakrishnan 79d9cce2e7 Fix ptr record link 2023-06-21 16:43:03 +05:30
Johannes Zellner 9fbfdd08d8 Update translation 2023-06-20 15:33:46 +02:00
Johannes Zellner 879569c661 filemanager: show busy state when extraction is in progress 2023-06-20 15:33:26 +02:00
Johannes Zellner 5814793dc1 filemanager: Integrate download and extract logic 2023-06-20 15:21:58 +02:00
Johannes Zellner 299e40c389 Allow cors for translation 2023-06-20 10:40:27 +02:00
Johannes Zellner 38860cd70c Redirect to / on dashboard 404 2023-06-19 15:02:28 +02:00
Johannes Zellner c8fe2611ba Also fix bottom bar for password reset 2023-06-19 14:08:10 +02:00
Johannes Zellner af9175b30c Better login action bar styling 2023-06-19 13:55:58 +02:00
Johannes Zellner 35453a0c2d Translate the oidc login view 2023-06-19 11:50:53 +02:00
Johannes Zellner fd91bf0498 Update translations 2023-06-18 20:19:12 +02:00
Johannes Zellner 3b02ef5591 filemanager: inject tr() for pankow 2023-06-18 20:11:48 +02:00
Johannes Zellner 2966763e9e filemanager: pankow has translation support 2023-06-18 18:35:55 +02:00
Johannes Zellner 6d7759a1af filemanager: add translation support 2023-06-18 17:39:40 +02:00
Johannes Zellner 70e7ca395d Update filemanager dependencies 2023-06-16 17:15:09 +02:00
Johannes Zellner 922c587ca9 Fix context menu closing with new pankow version 2023-06-16 17:13:45 +02:00
Johannes Zellner a555d70868 Add real info to filemanager readme 2023-06-16 12:49:47 +02:00
Johannes Zellner 6f6907363e Dashboard login view is gone and replaced with oidc 2023-06-15 18:05:06 +02:00
Girish Ramakrishnan 77d601f0cc mailbox: fix crash when editing quota of new mailboxes 2023-06-15 20:59:25 +05:30
Johannes Zellner 8e99f67fb7 use 'development' client only if apiOrigin template value is empty 2023-06-15 16:41:14 +02:00
Johannes Zellner 9d3fa94960 Add separate password reset view 2023-06-15 16:34:58 +02:00
Johannes Zellner b6739e9d77 Support local development dashboard login 2023-06-15 15:44:16 +02:00
Johannes Zellner 33c1b4ae3b oidc: also send profile with auth code
this helps us to be a bit more conforming with google and MS oidc
provider
2023-06-14 16:49:35 +02:00
Johannes Zellner 67c0a4f513 Copy selected terminal text with ctrl shift c 2023-06-13 15:27:16 +02:00
Johannes Zellner ce1181531a Update dashboard dependencies and fixup apps icon for new fontawesome 2023-06-13 13:54:34 +02:00
Girish Ramakrishnan 54682a1370 remove duplicate require 2023-06-04 18:23:26 +02:00
Girish Ramakrishnan dc5342b9fc automation tag is better 2023-06-04 18:18:22 +02:00
Girish Ramakrishnan 83bb7c475d add devops category 2023-06-04 18:11:34 +02:00
Johannes Zellner 638bdc902b Add implicit grants for dashboard 2023-06-04 17:39:31 +02:00
Johannes Zellner 874064de67 Only store dashboard accessTokens in tokensdb 2023-06-04 17:39:31 +02:00
Johannes Zellner 1f134ff070 Skip consent screen for dashboard login 2023-06-04 17:39:31 +02:00
Johannes Zellner 2c334170bd oidc dashboard login 2023-06-04 17:39:29 +02:00
Johannes Zellner 35efdf6cbd Support both sets of Hetzner nameservers 2023-05-31 18:25:09 +02:00
Girish Ramakrishnan e02f3d7064 Fix dashboard crash when installing app with no addons 2023-05-30 11:06:33 +02:00
Girish Ramakrishnan a5e83a4d84 Expose alias domains as CLOUDRON_ALIAS_DOMAINS
This can be useful for app to set them in trusted hosts. Or alternately,
show different text when accessed from different domains.
2023-05-25 11:47:41 +02:00
Girish Ramakrishnan e6ba2a6e7a replace usage of _.extend with Object.assign 2023-05-25 11:45:14 +02:00
Johannes Zellner 79dd50910c oidc: render error page instead of raw error body 2023-05-23 12:13:55 +02:00
Johannes Zellner c4d267ecb1 filemanager: add restart logic 2023-05-23 11:38:57 +02:00
Johannes Zellner 2011dd9a83 Explicitly add noto font to filemanager assets 2023-05-23 11:08:06 +02:00
Johannes Zellner b07131cd0f oidc: add password reset link to login view 2023-05-22 20:32:33 +02:00
Johannes Zellner d3fe165e2c oidc: Remove console.log in login screen 2023-05-22 20:19:30 +02:00
Johannes Zellner bf19de3a90 Fixup filemanager links 2023-05-22 16:27:48 +02:00
Johannes Zellner 58a0b3d8e7 Ensure localPath is quoted in case it contains spaces 2023-05-21 14:14:42 +02:00
Johannes Zellner 65c2ee1760 filemanager: Add logs and terminal links for apps 2023-05-16 17:48:53 +02:00
Johannes Zellner dfb0a7fee1 filemanager: update dependencies 2023-05-16 15:34:16 +02:00
Girish Ramakrishnan 7511339656 bump timeout when waiting for container
some server disks are very slow
2023-05-16 09:51:42 +02:00
Girish Ramakrishnan cb106f8a55 Fixup text when logs are missing 2023-05-16 09:36:30 +02:00
Girish Ramakrishnan 39d45b71d7 installer: remove user creation, already in init-ubuntu script 2023-05-15 21:10:29 +02:00
Girish Ramakrishnan db1fa84936 update: log history 2023-05-15 21:08:20 +02:00
Girish Ramakrishnan f83295372b updater: combine installer logs into the task file 2023-05-15 19:09:40 +02:00
Girish Ramakrishnan e6506d9458 updater: use log 2023-05-15 19:05:39 +02:00
Johannes Zellner af63dbb31d Show error when logs are gone 2023-05-15 17:49:34 +02:00
Johannes Zellner b5641cc445 Show at least basic error if task or app not found in logviewer 2023-05-15 17:20:43 +02:00
Johannes Zellner 576fb392bb Show dashboard domain change tasks like in other sections 2023-05-15 12:02:59 +02:00
Girish Ramakrishnan ff539e2669 remove crashnotifier
it's not really used
2023-05-15 11:08:00 +02:00
Girish Ramakrishnan 506d3adf70 Fix crash when querying backup mount status 2023-05-15 10:40:39 +02:00
Girish Ramakrishnan 94eb7849fe tasks: return 404 if task not found
part of #826
2023-05-15 10:16:00 +02:00
Johannes Zellner 9036b272a8 filemanager: update pankow module 2023-05-15 10:10:47 +02:00
Johannes Zellner c81467da7c filemanager: add refresh button 2023-05-15 09:57:58 +02:00
Johannes Zellner 6db3a20021 filemanager: support fallbackIcon 2023-05-15 09:26:37 +02:00
Johannes Zellner a428d6c553 filemanager: update dependencies 2023-05-15 09:02:31 +02:00
Girish Ramakrishnan b7b01d5605 domains: show current task in renewCert, syncDns 2023-05-14 11:47:21 +02:00
Girish Ramakrishnan 500d2361ec replace delay.js with timers/promises 2023-05-14 10:53:50 +02:00
Girish Ramakrishnan 75ba20201e Update modules 2023-05-14 07:23:04 +02:00
Girish Ramakrishnan b26c8d20cd network: add trusted ips
This allows the user to set trusted ips to Cloudflare or some other CDN
and have the logs have the correct IPs.

fixes #801
2023-05-13 16:15:47 +02:00
Girish Ramakrishnan 951ed4bf33 Update translations 2023-05-13 15:46:08 +02:00
Johannes Zellner 2a05ec3866 Move password-reveal.js to correct folder 2023-05-12 18:53:42 +02:00
Johannes Zellner 04f2bd1ec3 Add password-reveal feature to oidc login 2023-05-12 18:47:48 +02:00
Johannes Zellner e08116c9ad be more consistent in oidc login screen with dashboard login 2023-05-12 18:24:54 +02:00
Johannes Zellner da7fbeee3d oidc: Give proper login error feedback 2023-05-12 17:14:40 +02:00
Johannes Zellner 61aa32d8c5 App icon route is no open to public 2023-05-12 15:14:47 +02:00
Johannes Zellner 74ff5e8de4 Fix authorize for text in oidc consent screen 2023-05-12 14:01:20 +02:00
Johannes Zellner aad70a49b7 Remove dashboard button on oidc logout 2023-05-12 13:54:35 +02:00
Johannes Zellner d332bb05fa Show app name during oidc login 2023-05-12 13:51:50 +02:00
Johannes Zellner 6b6781eabb filemanager: vue is picky about the type 2023-05-12 13:32:51 +02:00
Girish Ramakrishnan 4a1cdd4ef1 Update aws-sdk and suppress maintenance mode message
https://github.com/aws/aws-sdk-js/issues/4354
2023-05-11 22:18:00 +02:00
Johannes Zellner 764a8f6a85 filemanager: Show non-dismissable dialog on fatal error 2023-05-11 18:36:09 +02:00
Johannes Zellner 22a0b84c2a filemanager: update dependencies 2023-05-11 16:45:13 +02:00
Johannes Zellner bba911165b Remove noisy openid debugs 2023-05-11 16:22:58 +02:00
Johannes Zellner 8656bea4f2 Update oidc-provider 2023-05-11 16:16:19 +02:00
Johannes Zellner 9024844449 Set favicon for OpenId views 2023-05-11 13:48:36 +02:00
Johannes Zellner 89c5b81eb0 Add very basic initial cloudron-logs helper 2023-05-11 12:30:00 +02:00
Johannes Zellner 18a7b0e615 dashboard: use sass instead of deprecated node-sass 2023-05-11 11:29:08 +02:00
Johannes Zellner 1407fbeb8c Fix syntax error in gulpfile 2023-05-11 10:57:52 +02:00
Johannes Zellner b5fc377dab Set app's fqdn as fallback logout redirect URI for oidc 2023-05-11 10:57:52 +02:00
Girish Ramakrishnan 71af16beb9 Update packages 2023-05-11 10:33:18 +02:00
Girish Ramakrishnan 96d3eda02b dashboard: update packages 2023-05-11 08:50:18 +02:00
Girish Ramakrishnan ba2a6bab68 dashboard: remove rimraf 2023-05-11 08:48:42 +02:00
Girish Ramakrishnan 092cc40da6 Fix test 2023-05-11 08:32:31 +02:00
Girish Ramakrishnan c55152c0e1 node: update to 18.16.0 2023-05-11 08:32:31 +02:00
Girish Ramakrishnan e83bb0c639 docker: update to 23.0.6 2023-05-11 08:32:31 +02:00
Johannes Zellner 318285cb07 Support pageSize customization via localStorage 2023-05-10 13:52:41 +02:00
Girish Ramakrishnan 5274e1c454 docker: registry finally has ipv6 support
https://github.com/docker/roadmap/issues/89
2023-05-10 10:14:25 +02:00
Girish Ramakrishnan 294a535c1b cloudron-support: better formatting of log link 2023-05-10 09:11:04 +02:00
Girish Ramakrishnan eaeb80e3c0 cloudron-support: add uname and lsb_release info 2023-05-10 09:08:04 +02:00
Johannes Zellner 6eb8047686 filemanager: open unsupported types in browser itself 2023-05-09 18:53:23 +02:00
Johannes Zellner db040bf293 There is no mail for filemanager 2023-05-09 10:58:29 +02:00
Girish Ramakrishnan acfc1ede6e add to changes 2023-05-09 10:55:22 +02:00
Girish Ramakrishnan 8910c76bcf Update redis to 7.0.11 2023-05-09 10:54:17 +02:00
Johannes Zellner 342093f661 filemanager: improve resource (app/volume/mail) handling 2023-05-08 18:08:11 +02:00
Johannes Zellner 9e26db3cd2 Only show disks with the correct fs type for volumes 2023-05-08 18:07:42 +02:00
Johannes Zellner a71b39ddee Start using the new filemanager 2023-05-08 16:09:33 +02:00
Johannes Zellner 0626354844 Fixup custom disk setup for volumes 2023-05-08 15:23:25 +02:00
Johannes Zellner e9d2a53aaf Add new ionos profitbricks regions 2023-05-08 14:04:46 +02:00
Girish Ramakrishnan ca59bbe1aa remove try/catch 2023-05-08 11:30:21 +02:00
Girish Ramakrishnan f505b1a553 remove log line which ends up in log file 2023-05-07 20:53:04 +02:00
Girish Ramakrishnan a237b11ff7 timezone: set default tz to UTC 2023-05-07 20:51:02 +02:00
Johannes Zellner 9a77f012d8 filemanager: Add path breadcrumbs and update dependencies 2023-05-07 17:04:07 +02:00
Johannes Zellner 36c7f779f3 filemanager: a symlink can't be opened 2023-05-07 13:50:41 +02:00
Girish Ramakrishnan b970e90178 cloudron-support: provider not needed 2023-05-05 17:18:38 +02:00
Johannes Zellner a7ea34914d Also put new task log style for backups view 2023-05-03 16:50:07 +02:00
Johannes Zellner 19e1e5861b provide more task logs for synDNS section 2023-05-03 16:33:19 +02:00
Girish Ramakrishnan e23777a642 kill a warning from npm 2023-05-03 09:15:16 +02:00
Girish Ramakrishnan a2f47f3ee2 7.5.0 changes 2023-05-02 23:08:42 +02:00
Girish Ramakrishnan 15e0f11bb9 acme: handle LE validation type cache logic
LE stores the validation type for 60 days. So, if we authorized via http previously,
we won't get a DNS challenge for that duration.

There are two ways to fix this:
* Deactivate the challenges - https://community.letsencrypt.org/t/authorization-deactivation/19860 and https://community.letsencrypt.org/t/deactivate-authorization/189526
* Just be able to handle dns or http challenge, whatever is asked. This is what this commit does. It prefers DNS challenge when possible

Other relevant threads:

https://community.letsencrypt.org/t/flush-of-authorization-cache/188043
https://community.letsencrypt.org/t/let-s-encrypt-s-vulnerability-as-a-feature-authz-reuse-and-eternal-account-key/21687
https://community.letsencrypt.org/t/http-01-validation-cache/22529
2023-05-02 23:07:32 +02:00
Johannes Zellner 1a32ea511e Use circle icons for task log status 2023-05-02 22:16:16 +02:00
Johannes Zellner ac602dc2a9 Give option to display last 10 cert renewal task logs 2023-05-02 16:55:57 +02:00
Johannes Zellner cf3fc940d2 Put all log viewer buttons in the section headers for the backup view 2023-05-02 15:02:41 +02:00
Johannes Zellner e09cac4ea1 Apply consisten section spacing to all views 2023-05-02 14:29:52 +02:00
Johannes Zellner 7c96115ea9 set constent section spacing in domains view 2023-05-02 14:12:27 +02:00
Johannes Zellner 12de353427 Make domains view also use uib-tooltips for consistency 2023-05-02 13:58:25 +02:00
Girish Ramakrishnan 057e4db6c1 use debug instead of console.error 2023-04-30 21:49:34 +02:00
Girish Ramakrishnan 883915c9d3 backups: move mount status to separate route 2023-04-30 17:21:18 +02:00
Girish Ramakrishnan 898413bfd4 convert console.log to debug 2023-04-30 10:18:48 +02:00
Girish Ramakrishnan aa02d839a7 remove console.log 2023-04-30 10:18:48 +02:00
Girish Ramakrishnan a4ba3a4dd0 import: backupConfig cannot be null 2023-04-30 10:18:48 +02:00
778 changed files with 15240 additions and 17100 deletions
+44
View File
@@ -2591,6 +2591,7 @@
* support: fix crash when opening tickets with 0 length files
[7.4.0]
* **IMPORTANT**: This is the last release of Cloudron to support Ubuntu 18.04. Please [upgrade](https://docs.cloudron.io/guides/upgrade-ubuntu-20/) to Ubuntu 20.04 (Focal Fossa) at the earliest.
* Update base image to jammy
* backups: Add idrive e2
* Support proxyAuth for proxy app
@@ -2628,3 +2629,46 @@
* Fix ipv4 vs ipv6 detection
* Fix misleading pending security updates message
[7.4.3]
* **IMPORTANT**: This is the last release of Cloudron to support Ubuntu 18.04. Please [upgrade](https://docs.cloudron.io/guides/upgrade-ubuntu-20/) to Ubuntu 20.04 (Focal Fossa) at the earliest.
* postgresql: fix for supporting Taiga with postgres 14
[7.5.0]
* **IMPORTANT**: This is the last release of Cloudron to support CPUs without AVX support. AVX support is required for MongoDB 5.0. See https://forum.cloudron.io/topic/8785/avx-support-in-your-vps-server for more information.
* acme: handle LE validation type cache logic
* improve viewing of logs
* redis: update to 7.0.11
* ionos profitbricks: add new regions Berlin and Logrono
* docker: update to 23.0.6
* network: trusted IPs
* mail: fix crash when editing quota of new mailboxes
* mail: update haraka to 3.0.2
* mail: fix issue where client IP was leaked in headers
* mail: skip SPF check of authenticated senders
* filemanager: new UI, support for large folders and lazy loading
* oidc: make UI translatable
* oidc: dashboard login uses oidc
* web terminal: Copy selected terminal text with ctrl shift c
* Expose alias domains as `CLOUDRON_ALIAS_DOMAINS`
[7.5.1]
* **IMPORTANT**: This is the last release of Cloudron to support CPUs without AVX support. AVX support is required for MongoDB 5.0. See https://forum.cloudron.io/topic/8785/avx-support-in-your-vps-server for more information.
* mail: Fix issue where mail usage sizes where reported incorrectly
* filemanager: Only init vue app after we fetch language files to avoid UI shaking
* mail: Clear the correct mail status notification
* filemanager: allow pasting on non-folders to cwd
* mail: give resolver more time
* dashboard: backup logs links are grayed out because of z-index
* branding: make oidc login does not use cloudron name
* translation: fix crash when translated text has single quote (french)
* dyndns: show logs
* mail: server location get it's own section
* optional services: redis & turn . joins sendmail, recvmail
* backups: encrypted backups must have .enc extension
* mail: add virtual all mail mailbox
* redirections: use 301 (permanent) instead of 302 (temporary) for redirections. this is better for SEO links
* graphs: show old backup size if > 1GB
* docker: fix image pruning
* Major overhaul of the REST API
* Fix import via SSHFS and CIFS
-11
View File
@@ -50,17 +50,6 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Development
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
tool to patch the VM with the latest code.
```
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
```
## License
Please note that the Cloudron code is under a source-available license. This is not the same as an
+2 -5
View File
@@ -9,7 +9,6 @@ const fs = require('fs'),
proxyAuth = require('./src/proxyauth.js'),
safe = require('safetydance'),
server = require('./src/server.js'),
settings = require('./src/settings.js'),
directoryServer = require('./src/directoryserver.js');
let logFd;
@@ -39,7 +38,7 @@ async function startServers() {
await proxyAuth.start();
await ldap.start();
const conf = await settings.getDirectoryServerConfig();
const conf = await directoryServer.getConfig();
if (conf.enabled) await directoryServer.start();
}
@@ -52,7 +51,7 @@ async function main() {
process.on('SIGHUP', async function () {
debug('Received SIGHUP. Re-reading configs.');
const conf = await settings.getDirectoryServerConfig();
const conf = await directoryServer.getConfig();
if (conf.enabled) await directoryServer.checkCertificate();
});
@@ -79,8 +78,6 @@ async function main() {
});
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
}
main();
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env node
'use strict';
const database = require('./src/database.js');
const crashNotifier = require('./src/crashnotifier.js');
// This is triggered by systemd with the crashed unit name as argument
async function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
const unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
// eventlog api needs the db
await database.initialize();
await crashNotifier.sendFailureLogs(unitName);
}
main();
+12 -71
View File
@@ -2,15 +2,15 @@
'use strict';
var argv = require('yargs').argv,
const argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
execSync = require('child_process').execSync,
fs = require('fs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass')(require('node-sass')),
sass = require('gulp-sass')(require('sass')),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps');
@@ -53,33 +53,11 @@ gulp.task('bootstrap', function () {
.pipe(gulp.dest('dist/3rdparty/js'));
});
gulp.task('monaco', function () {
return gulp.src('node_modules/monaco-editor/min/**/*')
.pipe(gulp.dest('dist/3rdparty/'));
});
gulp.task('moment', function () {
return gulp.src('node_modules/moment/min/*')
.pipe(gulp.dest('dist/3rdparty/js'));
});
gulp.task('xterm-core', function () {
return gulp.src('node_modules/xterm/**/*')
.pipe(gulp.dest('dist/3rdparty/xterm'));
});
gulp.task('xterm-addon-attach', function () {
return gulp.src('node_modules/xterm-addon-attach/**/*')
.pipe(gulp.dest('dist/3rdparty/xterm-addon-attach'));
});
gulp.task('xterm-addon-fit', function () {
return gulp.src('node_modules/xterm-addon-fit/**/*')
.pipe(gulp.dest('dist/3rdparty/xterm-addon-fit'));
});
gulp.task('xterm', gulp.series(['xterm-core', 'xterm-addon-attach', 'xterm-addon-fit']));
gulp.task('3rdparty-copy', function () {
return gulp.src([
'src/3rdparty/**/*.js',
@@ -94,7 +72,7 @@ gulp.task('3rdparty-copy', function () {
]).pipe(gulp.dest('dist/3rdparty/'));
});
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'monaco', 'xterm', 'bootstrap', 'fontawesome']));
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'moment', 'bootstrap', 'fontawesome']));
// --------------
// JavaScript
@@ -104,7 +82,6 @@ gulp.task('js-index', function () {
return gulp.src([
'src/js/index.js',
'src/js/client.js',
'src/js/main.js',
'src/js/utils.js',
'src/views/*.js'
])
@@ -115,38 +92,11 @@ gulp.task('js-index', function () {
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-logs', function () {
return gulp.src(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'])
gulp.task('js-passwordreset', function () {
return gulp.src(['src/js/passwordreset.js', 'src/js/utils.js'])
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-filemanager', function () {
return gulp.src(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'])
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('filemanager.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-terminal', function () {
return gulp.src(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'])
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-login', function () {
return gulp.src(['src/js/login.js', 'src/js/utils.js'])
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('login.js', { newLine: ';' }))
.pipe(concat('passwordreset.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
@@ -187,7 +137,7 @@ gulp.task('js-restore', function () {
.pipe(gulp.dest('dist/js'));
});
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
gulp.task('js', gulp.series([ 'js-index', 'js-passwordreset', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
// --------------
// HTML
@@ -197,10 +147,6 @@ gulp.task('html-views', function () {
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
});
gulp.task('html-components', function () {
return gulp.src('src/components/**/*.html').pipe(gulp.dest('dist/components'));
});
gulp.task('html-templates', function () {
return gulp.src('src/templates/**/*').pipe(gulp.dest('dist/templates'));
});
@@ -209,7 +155,7 @@ gulp.task('html-raw', function () {
return gulp.src('src/*.html').pipe(ejs({ apiOrigin: apiOrigin, revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
});
gulp.task('html', gulp.series(['html-views', 'html-components', 'html-templates', 'html-raw']));
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
// --------------
// CSS
@@ -245,8 +191,7 @@ gulp.task('timezones', function (done) {
// --------------
gulp.task('clean', function (done) {
rimraf.sync('dist');
done();
fs.rm('dist', { recursive: true, force: true }, done);
});
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'translation', 'images', 'css']));
@@ -257,18 +202,14 @@ gulp.task('watch', function (done) {
gulp.watch(['src/translation/*'], gulp.series(['translation']));
gulp.watch(['src/**/*.html'], gulp.series(['html']));
gulp.watch(['src/views/*.html'], gulp.series(['html-views']));
gulp.watch(['src/components/*.html'], gulp.series(['html-components']));
gulp.watch(['src/templates/*.html'], gulp.series(['html-templates']));
gulp.watch(['scripts/createTimezones.js', 'src/js/utils.js'], gulp.series(['timezones']));
gulp.watch(['src/js/setup.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setup']));
gulp.watch(['src/js/setupdns.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-setupdns']));
gulp.watch(['src/js/restore.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-restore']));
gulp.watch(['src/js/logs.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-logs']));
gulp.watch(['src/js/filemanager.js', 'src/js/client.js', 'src/js/utils.js', 'src/components/*.js'], gulp.series(['js-filemanager']));
gulp.watch(['src/js/terminal.js', 'src/js/client.js', 'src/js/utils.js'], gulp.series(['js-terminal']));
gulp.watch(['src/js/login.js', 'src/js/utils.js'], gulp.series(['js-login']));
gulp.watch(['src/js/passwordreset.js', 'src/js/utils.js'], gulp.series(['js-passwordreset']));
gulp.watch(['src/js/setupaccount.js', 'src/js/utils.js'], gulp.series(['js-setupaccount']));
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/main.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/views/*.js', 'src/js/utils.js'], gulp.series(['js-index']));
gulp.watch(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
done();
});
+329 -3800
View File
File diff suppressed because it is too large Load Diff
+5 -10
View File
@@ -13,9 +13,9 @@
"author": "",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"bootstrap-sass": "^3.4.1",
"chart.js": "^4.1.1",
"@fortawesome/fontawesome-free": "^6.4.0",
"bootstrap-sass": "^3.4.3",
"chart.js": "^4.3.0",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^8.0.0",
"gulp-concat": "^2.6.1",
@@ -25,13 +25,8 @@
"gulp-serve": "^1.4.0",
"gulp-sourcemaps": "^3.0.0",
"moment": "^2.29.4",
"monaco-editor": "^0.34.0",
"node-sass": "^7.0.3",
"rimraf": "^3.0.2",
"xterm": "^5.1.0",
"xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0",
"yargs": "^17.5.1"
"sass": "^1.63.3",
"yargs": "^17.7.2"
},
"eslintConfig": {
"env": {
+2 -2
View File
@@ -24,7 +24,7 @@ function getAccessToken(callback) {
let username = readlineSync.question('Username: ', {});
let password = readlineSync.question('Password: ', { noEchoBack: true });
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.log('Login failed');
return getAccessToken(callback);
@@ -63,4 +63,4 @@ getAccessToken(function (accessToken) {
console.log('Done');
});
});
});
+2 -2
View File
@@ -20,7 +20,7 @@ function getAccessToken(callback) {
let username = readlineSync.question('Username: ', {});
let password = readlineSync.question('Password: ', { noEchoBack: true });
superagent.post(`https://${cloudronDomain}/api/v1/cloudron/login`, { username: username, password: password }).end(function (error, result) {
superagent.post(`https://${cloudronDomain}/api/v1/auth/login`, { username: username, password: password }).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.log('Login failed');
return getAccessToken(callback);
@@ -66,4 +66,4 @@ getAccessToken(function (accessToken) {
});
});
});
});
File diff suppressed because one or more lines are too long
-639
View File
@@ -1,639 +0,0 @@
(function($, angular) {
// eslint-disable-next-line angular/file-name, angular/no-service-method
angular.module('ui.bootstrap.contextMenu', [])
.service('CustomService', function () {
'use strict';
return {
initialize: function (item) {
console.log('got here', item);
}
};
})
.constant('ContextMenuEvents', {
// Triggers when all the context menus have been closed
ContextMenuAllClosed: 'context-menu-all-closed',
// Triggers when any single conext menu is called.
// Closing all context menus triggers this for each level open
ContextMenuClosed: 'context-menu-closed',
// Triggers right before the very first context menu is opened
ContextMenuOpening: 'context-menu-opening',
// Triggers right after any context menu is opened
ContextMenuOpened: 'context-menu-opened'
})
.directive('contextMenu', ['$rootScope', 'ContextMenuEvents', '$parse', '$q', 'CustomService', '$sce', '$document', '$window', '$compile',
function ($rootScope, ContextMenuEvents, $parse, $q, custom, $sce, $document, $window, $compile) {
var _contextMenus = [];
// Contains the element that was clicked to show the context menu
var _clickedElement = null;
var DEFAULT_ITEM_TEXT = '"New Item';
var _emptyText = 'empty';
function createAndAddOptionText(params) {
// Destructuring:
var $scope = params.$scope;
var item = params.item;
var event = params.event;
var modelValue = params.modelValue;
var $promises = params.$promises;
var nestedMenu = params.nestedMenu;
var $li = params.$li;
var leftOriented = String(params.orientation).toLowerCase() === 'left';
var optionText = null;
if (item.html) {
if (angular.isFunction(item.html)) {
// runs the function that expects a jQuery/jqLite element
optionText = item.html($scope);
} else {
// Incase we want to compile html string to initialize their custom directive in html string
if (item.compile) {
optionText = $compile(item.html)($scope);
} else {
// Assumes that the developer already placed a valid jQuery/jqLite element
optionText = item.html;
}
}
} else {
var $a = $('<a>');
var $anchorStyle = {};
if (leftOriented) {
$anchorStyle.textAlign = 'right';
$anchorStyle.paddingLeft = '8px';
} else {
$anchorStyle.textAlign = 'left';
$anchorStyle.paddingRight = '8px';
}
$a.css($anchorStyle);
$a.addClass('dropdown-item');
$a.attr({ tabindex: '-1', href: '#' });
var textParam = item.text || item[0];
var text = DEFAULT_ITEM_TEXT;
if (typeof textParam === 'string') {
text = textParam;
} else if (typeof textParam === 'function') {
text = textParam.call($scope, $scope, event, modelValue);
}
var $promise = $q.when(text);
$promises.push($promise);
$promise.then(function (pText) {
if (nestedMenu) {
var $arrow;
var $boldStyle = {
fontFamily: 'monospace',
fontWeight: 'bold'
};
if (leftOriented) {
$arrow = '&lt;';
$boldStyle.float = 'left';
} else {
$arrow = '&gt;';
$boldStyle.float = 'right';
}
var $bold = $('<strong style="font-family:monospace;font-weight:bold;float:right;">' + $arrow + '</strong>');
$bold.css($boldStyle);
$a.css('cursor', 'default');
$a.append($bold);
}
$a.append(pText);
});
optionText = $a;
}
$li.append(optionText);
return optionText;
};
/**
* Process each individual item
*
* Properties of params:
* - $scope
* - event
* - modelValue
* - level
* - item
* - $ul
* - $li
* - $promises
*/
function processItem(params) {
var nestedMenu = extractNestedMenu(params);
// if html property is not defined, fallback to text, otherwise use default text
// if first item in the item array is a function then invoke .call()
// if first item is a string, then text should be the string.
var text = DEFAULT_ITEM_TEXT;
var currItemParam = angular.extend({}, params);
var item = params.item;
var enabled = item.enabled === undefined ? item[2] : item.enabled;
currItemParam.nestedMenu = nestedMenu;
currItemParam.enabled = resolveBoolOrFunc(enabled, params);
currItemParam.text = createAndAddOptionText(currItemParam);
registerCurrentItemEvents(currItemParam);
};
/*
* Registers the appropriate mouse events for options if the item is enabled.
* Otherwise, it ensures that clicks to the item do not propagate.
*/
function registerCurrentItemEvents (params) {
// Destructuring:
var item = params.item;
var $ul = params.$ul;
var $li = params.$li;
var $scope = params.$scope;
var modelValue = params.modelValue;
var level = params.level;
var event = params.event;
var text = params.text;
var nestedMenu = params.nestedMenu;
var enabled = params.enabled;
var orientation = String(params.orientation).toLowerCase();
var customClass = params.customClass;
if (enabled) {
var openNestedMenu = function ($event) {
removeContextMenus(level + 1);
/*
* The object here needs to be constructed and filled with data
* on an "as needed" basis. Copying the data from event directly
* or cloning the event results in unpredictable behavior.
*/
/// adding the original event in the object to use the attributes of the mouse over event in the promises
var ev = {
pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
// eslint-disable-next-line angular/window-service
view: event.view || window,
target: event.target,
event: $event
};
/*
* At this point, nestedMenu can only either be an Array or a promise.
* Regardless, passing them to `when` makes the implementation singular.
*/
$q.when(nestedMenu).then(function(promisedNestedMenu) {
if (angular.isFunction(promisedNestedMenu)) {
// support for dynamic subitems
promisedNestedMenu = promisedNestedMenu.call($scope, $scope, event, modelValue, text, $li);
}
var nestedParam = {
$scope : $scope,
event : ev,
options : promisedNestedMenu,
modelValue : modelValue,
level : level + 1,
orientation: orientation,
customClass: customClass
};
renderContextMenu(nestedParam);
});
};
$li.on('click', function ($event) {
if($event.which == 1) {
$event.preventDefault();
$scope.$apply(function () {
var cleanupFunction = function () {
$(event.currentTarget).removeClass('context');
removeAllContextMenus();
};
var clickFunction = angular.isFunction(item.click)
? item.click
: (angular.isFunction(item[1])
? item[1]
: null);
if (clickFunction) {
var res = clickFunction.call($scope, $scope, event, modelValue, text, $li);
if(res === undefined || res) {
cleanupFunction();
}
} else {
cleanupFunction();
}
});
}
});
$li.on('mouseover', function ($event) {
$scope.$apply(function () {
if (nestedMenu) {
openNestedMenu($event);
} else {
removeContextMenus(level + 1);
}
});
});
} else {
setElementDisabled($li);
}
};
/**
* @param params - an object containing the `item` parameter
* @returns an Array or a Promise containing the children,
* or null if the option has no submenu
*/
function extractNestedMenu(params) {
// Destructuring:
var item = params.item;
// New implementation:
if (item.children) {
if (angular.isFunction(item.children)) {
// Expects a function that returns a Promise or an Array
return item.children();
} else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
// Returns the promise
// OR, returns the actual array
return item.children;
}
return null;
} else {
// nestedMenu is either an Array or a Promise that will return that array.
// NOTE: This might be changed soon as it's a hangover from an old implementation
return angular.isArray(item[1]) ||
(item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
(item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
(item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
}
}
/**
* Responsible for the actual rendering of the context menu.
*
* The parameters in params are:
* - $scope = the scope of this context menu
* - event = the event that triggered this context menu
* - options = the options for this context menu
* - modelValue = the value of the model attached to this context menu
* - level = the current context menu level (defauts to 0)
* - customClass = the custom class to be used for the context menu
*/
function renderContextMenu (params) {
/// <summary>Render context menu recursively.</summary>
// Destructuring:
var $scope = params.$scope;
var event = params.event;
var options = params.options;
var modelValue = params.modelValue;
var level = params.level;
var customClass = params.customClass;
// Initialize the container. This will be passed around
var $ul = initContextMenuContainer(params);
params.$ul = $ul;
// Register this level of the context menu
_contextMenus.push($ul);
/*
* This object will contain any promises that we have
* to wait for before trying to adjust the context menu.
*/
var $promises = [];
params.$promises = $promises;
angular.forEach(options, function (item) {
if (item === null) {
appendDivider($ul);
} else {
// If displayed is anything other than a function or a boolean
var displayed = resolveBoolOrFunc(item.displayed, params);
// Only add the <li> if the item is displayed
if (displayed) {
var $li = $('<li>');
var itemParams = angular.extend({}, params);
itemParams.item = item;
itemParams.$li = $li;
if (typeof item[0] === 'object') {
custom.initialize($li, item);
} else {
processItem(itemParams);
}
if (resolveBoolOrFunc(item.hasTopDivider, itemParams, false)) {
appendDivider($ul);
}
$ul.append($li);
if (resolveBoolOrFunc(item.hasBottomDivider, itemParams, false)) {
appendDivider($ul);
}
}
}
});
if ($ul.children().length === 0) {
var $emptyLi = angular.element('<li>');
setElementDisabled($emptyLi);
$emptyLi.html('<a>' + _emptyText + '</a>');
$ul.append($emptyLi);
}
$document.find('body').append($ul);
doAfterAllPromises(params);
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpened, {
context: _clickedElement,
contextMenu: $ul,
params: params
});
};
/**
* calculate if drop down menu would go out of screen at left or bottom
* calculation need to be done after element has been added (and all texts are set; thus the promises)
* to the DOM the get the actual height
*/
function doAfterAllPromises (params) {
// Desctructuring:
var $ul = params.$ul;
var $promises = params.$promises;
var level = params.level;
var event = params.event;
var leftOriented = String(params.orientation).toLowerCase() === 'left';
$q.all($promises).then(function () {
var topCoordinate = event.pageY;
var menuHeight = angular.element($ul[0]).prop('offsetHeight');
var winHeight = $window.pageYOffset + event.view.innerHeight;
/// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
topCoordinate = event.pageY - menuHeight;
/// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
if(level && level > 0) {
topCoordinate += event.event.currentTarget.offsetHeight;
}
} else if(winHeight <= menuHeight) {
// If it really can't fit, reset the height of the menu to one that will fit
angular.element($ul[0]).css({ 'height': winHeight - 5, 'overflow-y': 'scroll' });
// ...then set the topCoordinate height to 0 so the menu starts from the top
topCoordinate = 0;
} else if(winHeight - topCoordinate < menuHeight) {
var reduceThresholdY = 5;
if(topCoordinate < reduceThresholdY) {
reduceThresholdY = topCoordinate;
}
topCoordinate = winHeight - menuHeight - reduceThresholdY;
}
var leftCoordinate = event.pageX;
var menuWidth = angular.element($ul[0]).prop('offsetWidth');
var winWidth = event.view.innerWidth + window.pageXOffset;
var padding = 5;
if (leftOriented) {
if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
leftCoordinate = padding;
} else if (leftCoordinate < menuWidth) {
var reduceThresholdX = 5;
if (winWidth - leftCoordinate < reduceThresholdX + padding) {
reduceThresholdX = winWidth - leftCoordinate + padding;
}
leftCoordinate = menuWidth + reduceThresholdX + padding;
} else {
leftCoordinate = leftCoordinate - menuWidth;
}
} else {
if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
leftCoordinate = winWidth - menuWidth - padding;
} else if(winWidth - leftCoordinate < menuWidth) {
var reduceThresholdX = 5;
if(leftCoordinate < reduceThresholdX + padding) {
reduceThresholdX = leftCoordinate + padding;
}
leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
}
}
$ul.css({
display: 'block',
position: 'absolute',
left: leftCoordinate + 'px',
top: topCoordinate + 'px'
});
});
};
/**
* Creates the container of the context menu (a <ul> element),
* applies the appropriate styles and then returns that container
*
* @return a <ul> jqLite/jQuery element
*/
function initContextMenuContainer(params) {
// Destructuring
var customClass = params.customClass;
var $ul = $('<ul>');
$ul.addClass('dropdown-menu');
$ul.attr({ 'role': 'menu' });
$ul.css({
display: 'block',
position: 'absolute',
left: params.event.pageX + 'px',
top: params.event.pageY + 'px',
'z-index': 10000
});
if(customClass) { $ul.addClass(customClass); }
return $ul;
}
function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
}
/**
* Removes the context menus with level greater than or equal
* to the value passed. If undefined, null or 0, all context menus
* are removed.
*/
function removeContextMenus (level) {
while (_contextMenus.length && (!level || _contextMenus.length > level)) {
var cm = _contextMenus.pop();
$rootScope.$broadcast(ContextMenuEvents.ContextMenuClosed, { context: _clickedElement, contextMenu: cm });
cm.remove();
}
if(!level) {
$rootScope.$broadcast(ContextMenuEvents.ContextMenuAllClosed, { context: _clickedElement });
}
}
function removeOnScrollEvent(e) {
removeAllContextMenus(e);
}
function removeOnOutsideClickEvent(e) {
var $curr = $(e.target);
var shouldRemove = true;
while($curr.length) {
if ($curr.hasClass('dropdown-menu')) {
shouldRemove = false;
break;
} else {
$curr = $curr.parent();
}
}
if (shouldRemove) {
removeAllContextMenus(e);
}
}
function removeAllContextMenus(e) {
$document.find('body').off('mousedown touchstart', removeOnOutsideClickEvent);
$document.off('scroll', removeOnScrollEvent);
$(_clickedElement).removeClass('context');
removeContextMenus();
$rootScope.$broadcast('');
}
function isBoolean(a) {
return a === false || a === true;
}
/** Resolves a boolean or a function that returns a boolean
* Returns true by default if the param is null or undefined
* @param a - the parameter to be checked
* @param params - the object for the item's parameters
* @param defaultValue - the default boolean value to use if the parameter is
* neither a boolean nor function. True by default.
*/
function resolveBoolOrFunc(a, params, defaultValue) {
var item = params.item;
var $scope = params.$scope;
var event = params.event;
var modelValue = params.modelValue;
defaultValue = isBoolean(defaultValue) ? defaultValue : true;
if (isBoolean(a)) {
return a;
} else if (angular.isFunction(a)) {
return a.call($scope, $scope, event, modelValue);
} else {
return defaultValue;
}
}
function appendDivider($ul) {
var $li = angular.element('<li>');
$li.addClass('divider');
$ul.append($li);
}
function setElementDisabled($li) {
$li.on('click', function ($event) {
$event.preventDefault();
});
$li.addClass('disabled');
}
return function ($scope, element, attrs) {
var openMenuEvents = ['contextmenu'];
_emptyText = $scope.$eval(attrs.contextMenuEmptyText) || 'empty';
if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === 'string'){
openMenuEvents = attrs.contextMenuOn.split(',');
}
angular.forEach(openMenuEvents, function (openMenuEvent) {
element.on(openMenuEvent.trim(), function (event) {
// Cleanup any leftover contextmenus(there are cases with longpress on touch where we
// still see multiple contextmenus)
removeAllContextMenus();
if(!attrs.allowEventPropagation) {
event.stopPropagation();
event.preventDefault();
}
// Don't show context menu if on touch device and element is draggable
if(isTouchDevice() && element.attr('draggable') === 'true') {
return false;
}
// Remove if the user clicks outside
$document.find('body').on('mousedown touchstart', removeOnOutsideClickEvent);
// Remove the menu when the scroll moves
$document.on('scroll', removeOnScrollEvent);
_clickedElement = event.currentTarget;
$(_clickedElement).addClass('context');
$scope.$apply(function () {
var options = $scope.$eval(attrs.contextMenu);
var customClass = attrs.contextMenuClass;
var modelValue = $scope.$eval(attrs.model);
var orientation = attrs.contextMenuOrientation;
$q.when(options).then(function(promisedMenu) {
if (angular.isFunction(promisedMenu)) {
// support for dynamic items
promisedMenu = promisedMenu.call($scope, $scope, event, modelValue);
}
var params = {
'$scope' : $scope,
'event' : event,
'options' : promisedMenu,
'modelValue' : modelValue,
'level' : 0,
'customClass' : customClass,
'orientation': orientation
};
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpening, { context: _clickedElement });
renderContextMenu(params);
});
});
// Remove all context menus if the scope is destroyed
$scope.$on('$destroy', function () {
removeAllContextMenus();
});
});
});
if (attrs.closeMenuOn) {
$scope.$on(attrs.closeMenuOn, function () {
removeAllContextMenus();
});
}
};
}]);
// eslint-disable-next-line angular/window-service
})(window.angular.element, window.angular);
+32
View File
@@ -0,0 +1,32 @@
// Custom library to add password show/hide icons to input element with `password-reveal` attribute
// util.js has the angular version, this is for plain js
window.addEventListener('load', function () {
var svgEye = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>';
var svgEyeSlash = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>';
document.querySelectorAll('[password-reveal]').forEach(function (element) {
var eye = document.createElement('i');
eye.innerHTML = svgEyeSlash;
eye.style.width = '18px';
eye.style.height = '18px';
eye.style.position = 'relative';
eye.style.float = 'right';
eye.style.marginTop = '-24px';
eye.style.marginRight = '10px';
eye.style.cursor = 'pointer';
eye.addEventListener('click', function () {
if (element.type === 'password') {
element.type = 'text';
eye.innerHTML = svgEye;
} else {
element.type = 'password';
eye.innerHTML = svgEyeSlash;
}
});
element.parentNode.style.position = 'relative';
element.parentNode.insertBefore(eye, element.nextSibling);
});
});
+11
View File
@@ -0,0 +1,11 @@
<script>
var tmp = window.location.hash.slice(1).split('&');
tmp.forEach(function (pair) {
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
});
window.location.href = '/';
</script>
-80
View File
@@ -1,80 +0,0 @@
<!-- Modal image/video viewer -->
<div class="modal fade" id="{{ 'mediaViewerModal-' + $id }}" tabindex="-1" role="dialog">
<div class="modal-dialog" style="max-width: 1280px; max-height: calc(100% - 60px);">
<div class="modal-content" style="height: 100%; height: 100%; display: flex; background-color: #000; background-clip: border-box;">
<img ng-show="mediaViewer.type === 'image'" ng-src="{{ mediaViewer.src }}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;" />
<video ng-show="mediaViewer.type === 'video'" controls preload="auto" autoplay ng-src="{{ mediaViewer.src | trustUrl}}" style="display: block; margin: auto; max-width: 100%; max-height: 100%;"></video>
</div>
</div>
</div>
<!-- main content -->
<div class="toolbar">
<div class="btn-group" role="group" style="display: block;">
<!-- TODO figure out why a line break in code between the two buttons results in a gap visually without any margin/padding set -->
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="cwd === ''"><i class="fas fa-arrow-left"></i></button><button class="btn btn-primary" ng-disabled="busyRefresh" ng-click="refresh()"><i class="fas fa-redo" ng-class="{ 'fa-spin': busyRefresh }"></i></button>
</div>
<div class="btn-group path-parts" role="group">
<button class="btn btn-default" ng-disabled="cwd === ''" ng-click="changeDirectory('/')" ng-drop="drop($event, '/')" ng-dragleave="dragExit($event, '/')" ng-dragover="dragEnter($event, '/')"><i class="fas fa-home"></i> {{ rootDirLabel }} </button><button class="btn btn-default" ng-disabled="part.path === cwd" ng-click="changeDirectory(part.path)" ng-drop="drop($event, part.path)" ng-dragleave="dragExit($event, part.path)" ng-dragover="dragEnter($event, part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
</div>
<div style="display: block;">
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-plus"></i> {{ 'filemanager.toolbar.new' | tr }}</button>
<ul class="dropdown-menu">
<li><a class="hand" ng-click="onNewFile()">{{ 'filemanager.toolbar.newFile' | tr }}</a></li>
<li><a class="hand" ng-click="onNewFolder()">{{ 'filemanager.toolbar.newFolder' | tr }}</a></li>
</ul>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-upload"></i> {{ 'filemanager.toolbar.upload' | tr }}</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a class="hand" ng-click="onUploadFile()">{{ 'filemanager.toolbar.uploadFile' | tr }}</a></li>
<li><a class="hand" ng-click="onUploadFolder()">{{ 'filemanager.toolbar.uploadFolder' | tr }}</a></li>
</ul>
</div>
</div>
</div>
<div class="file-list-header">
<table class="table" style="margin: 0;">
<thead>
<tr>
<th style="width: 42px">&nbsp;</th>
<th style="">{{ 'filemanager.list.name' | tr }}</th>
<th style="width:100px">{{ 'filemanager.list.owner' | tr }}</th>
<th style="width: 80px">{{ 'filemanager.list.size' | tr }}</th>
<th style="width:100px">{{ 'filemanager.list.mtime' | tr }}</th>
<th style="width: 45px;">&nbsp;</th>
</tr>
</thead>
</table>
</div>
<div class="file-list" ng-class="{ 'entry-hovered': dropToBody, 'busy': busy }" context-menu="menuOptionsBlank" ng-mousedown="onClearSelection($event)" ng-drop="drop($event, null)" ng-dragleave="dragExit($event, null)" ng-dragover="dragEnter($event, null)">
<table class="table table-hover" style="margin: 0;">
<tbody>
<tr ng-show="busy && !busyRefresh">
<td colspan="6"><center><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center></td>
</tr>
<tr ng-show="!(busy && !busyRefresh) && entries.length === 0">
<td colspan="" class="text-center">{{ 'filemanager.list.empty' | tr }}</td>
</tr>
<tr style="cursor: default" ng-hide="busy && !busyRefresh" entry-hashkey="{{ entry['$$hashKey'] }}" ng-repeat="entry in entries" ng-mouseup="onMouseup($event, entry)" draggable="true" ng-dragstart="dragStart($event, entry)" ng-drop="drop($event, entry)" context-menu="menuOptions" ng-mousedown="onMousedown($event, entry)" model="entry" ng-dragleave="dragExit($event, entry)" ng-dragover="dragEnter($event, entry)" ng-class="{ 'entry-hovered': entry.hovered, 'entry-selected': isSelected(entry) }">
<td style="width: 42px; height: 42px" ng-dblclick="open(entry)" class="text-center">
<i ng-show="!entry.previewUrl" class="fas fa-lg {{ entry.icon }}" ng-class="{ 'text-primary': entry.isDirectory && !isSelected(entry) }"></i>
<img ng-show="entry.previewUrl" ng-src="{{ entry.previewUrl }}" height="42" width="42" style="object-fit: cover;"/>
</td>
<td class="elide-table-cell" ng-dblclick="open(entry)" style="padding-left: 5px;">{{ entry.fileName }}<span ng-show="entry.isSymbolicLink" class="text-muted" style="margin-left: 20px;">{{ 'filemanager.list.symlink' | tr:{ target: entry.target } }}</span></td>
<td style="width:100px" class="elide-table-cell" ng-dblclick="open(entry)">{{ entry.uid | prettyOwner }}</td>
<td style="width: 80px" class="elide-table-cell" ng-dblclick="open(entry)">{{ entry.size | prettyDecimalSize }}</td>
<td style="width:100px" class="elide-table-cell" ng-dblclick="open(entry)" uib-tooltip="{{ entry.mtime | prettyLongDate }}" tooltip-append-to-body="true">{{ entry.mtime | prettyDate }}</td>
<td style="width: 45px">
<button type="button" class="btn btn-xs btn-default context-menu-action" context-menu="menuOptions" model="entry" context-menu-on="click" ng-click="onEntryContextMenu($event, entry)"><i class="fas fa-ellipsis-h"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
-513
View File
@@ -1,513 +0,0 @@
'use strict';
/* global angular */
/* global sanitize, isModalVisible */
angular.module('Application').component('filetree', {
bindings: {
backendId: '<',
backendType: '<',
view: '<',
clipboard: '<',
onUploadFile: '&',
onUploadFolder: '&',
onNewFile: '&',
onNewFolder: '&',
onRenameEntry: '&',
onExtractEntry: '&',
onChownEntries: '&',
onDeleteEntries: '&',
onCopyEntries: '&',
onCutEntries: '&',
onPasteEntries: '&'
},
templateUrl: 'components/filetree.html?<%= revision %>',
controller: [ '$scope', '$translate', '$timeout', 'Client', FileTreeController ]
});
function FileTreeController($scope, $translate, $timeout, Client) {
var ctrl = this;
$scope.backendId = this.backendId;
$scope.backendType = this.backendType;
$scope.view = this.view;
$scope.busy = true;
$scope.busyRefresh = false;
$scope.client = Client;
$scope.cwd = null;
$scope.cwdParts = [];
$scope.rootDirLabel = '';
$scope.entries = [];
$scope.selected = []; // holds selected entries
$scope.dropToBody = false;
$scope.applicationLink = '';
// register so parent can call child
$scope.$parent.registerChild($scope);
function isArchive(f) {
return f.match(/\.tgz$/) ||
f.match(/\.tar$/) ||
f.match(/\.7z$/) ||
f.match(/\.zip$/) ||
f.match(/\.tar\.gz$/) ||
f.match(/\.tar\.xz$/) ||
f.match(/\.tar\.bz2$/);
}
$scope.menuOptions = []; // shown for entries
$scope.menuOptionsBlank = []; // shown for empty space in folder
function sort() {
return $scope.entries.sort(function (a, b) {
if (a.fileName.toLowerCase() < b.fileName.toLowerCase()) return -1;
return 1;
}).sort(function (a, b) {
if ((a.isDirectory && b.isDirectory) || (!a.isDirectory && !b.isDirectory)) return 0;
if (a.isDirectory && !b.isDirectory) return -1;
return 1;
});
}
$scope.isSelected = function (entry) {
return $scope.selected.indexOf(entry) !== -1;
};
function download(entry) {
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
Client.filesGet($scope.backendId, $scope.backendType, filePath, 'download', function (error) {
if (error) return Client.error(error);
});
}
$scope.dragStart = function ($event, entry) {
var filePaths = $scope.selected.map(function (entry) { return sanitize($scope.cwd + '/' + entry.fileName); });
$event.originalEvent.dataTransfer.setData('application/cloudron-filemanager', JSON.stringify(filePaths));
};
$scope.dragEnter = function ($event, entry) {
$event.originalEvent.stopPropagation();
$event.originalEvent.preventDefault();
// if entry is string, we come from breadcrumb
if (entry && typeof entry === 'string') $event.currentTarget.classList.add('entry-hovered');
else if (entry && entry.isDirectory) entry.hovered = true;
else $scope.dropToBody = true;
$event.originalEvent.dataTransfer.dropEffect = 'move';
};
$scope.dragExit = function ($event, entry) {
$event.originalEvent.stopPropagation();
$event.originalEvent.preventDefault();
// if entry is string, we come from breadcrumb
if (entry && typeof entry === 'string') $event.currentTarget.classList.remove('entry-hovered');
else if (entry && entry.isDirectory) entry.hovered = false;
$scope.dropToBody = false;
$event.originalEvent.dataTransfer.dropEffect = 'move';
};
$scope.drop = function (event, entry) {
event.originalEvent.stopPropagation();
event.originalEvent.preventDefault();
$scope.dropToBody = false;
if (!event.originalEvent.dataTransfer.items[0]) return;
var targetFolder;
if (entry === null) targetFolder = $scope.cwd + '/';
else if (typeof entry === 'string') targetFolder = sanitize(entry);
else targetFolder = sanitize($scope.cwd + '/' + (entry && entry.isDirectory ? entry.fileName : ''));
var dataTransfer = event.originalEvent.dataTransfer;
var dragContent = dataTransfer.getData('application/cloudron-filemanager');
// check if we have internal drag'n'drop
if (dragContent) {
var moved = 0;
// we expect a JSON.stringified Array here
try {
dragContent = JSON.parse(dragContent);
} catch (e) {
console.error('Wrong drag content.', e);
return;
}
// move files
async.eachLimit(dragContent, 5, function (oldFilePath, callback) {
var fileName = oldFilePath.split('/').slice(-1);
var newFilePath = sanitize(targetFolder + '/' + fileName);
// if we drop the item on itself
if (oldFilePath === targetFolder) return callback();
// if nothing changes
if (newFilePath === oldFilePath) return callback();
moved++;
// TODO this will overwrite files in destination!
Client.filesRename($scope.backendId, $scope.backendType, oldFilePath, newFilePath, callback);
}, function (error) {
if (error) return Client.error(error);
// only refresh if anything has changed
if (moved) $scope.refresh();
});
return;
}
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
var folderItem;
try {
folderItem = dataTransfer.items[0].webkitGetAsEntry();
if (folderItem.isFile) return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
} catch (e) {
return $scope.$parent.uploadFiles(event.originalEvent.dataTransfer.files, targetFolder, false);
}
// if we got here we have a folder drop and a modern browser
// now traverse the folder tree and create a file list
var fileList = [];
function traverseFileTree(item, path, callback) {
if (item.isFile) {
// Get file
item.file(function (file) {
fileList.push(file);
callback();
});
} else if (item.isDirectory) {
// Get folder contents
var dirReader = item.createReader();
dirReader.readEntries(function (entries) {
async.each(entries, function (entry, callback) {
traverseFileTree(entry, path + item.name + '/', callback);
}, callback);
});
}
}
traverseFileTree(folderItem, '', function (error) {
if (error) return console.error(error);
$scope.$parent.uploadFiles(fileList, targetFolder, false);
});
};
$scope.refresh = function () {
$scope.$parent.refresh();
};
function amendIcons() {
$scope.entries.forEach(function (e) {
e.icon = 'fa-file';
e.previewUrl = null;
if (e.isDirectory) e.icon = 'fa-folder';
if (e.isSymbolicLink) e.icon = 'fa-link';
if (e.isFile) {
var mimeType = Mimer().get(e.fileName.toLowerCase());
var mimeGroup = mimeType.split('/')[0];
if (mimeGroup === 'text') e.icon = 'fa-file-alt';
// if (mimeGroup === 'image') e.icon = 'fa-file-image';
if (mimeGroup === 'image') {
e.icon = 'fa-file-image';
e.previewUrl = Client.filesGetLink($scope.backendId, $scope.backendType, sanitize($scope.cwd + '/' + e.fileName));
}
if (mimeGroup === 'video') e.icon = 'fa-file-video';
if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
}
});
}
// called from the parent
$scope.onRefresh = function () {
$scope.selected = [];
$scope.busy = true;
$scope.busyRefresh = true;
Client.filesGet($scope.backendId, $scope.backendType, $scope.cwd, 'data', function (error, result) {
if (error && error.statusCode !== 404) return Client.error(error);
$scope.entries = result ? result.entries : [];
amendIcons();
sort();
$scope.busyRefresh = false;
$scope.busy = false;
});
};
function openDirectory(path) {
$scope.cwd = path;
$scope.selected = [];
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: decodeURIComponent(p), path: path.split('/').slice(0, i+1).join('/') }; });
// refresh will set busy to false once done
$scope.refresh();
}
function openFile(entry) {
var mimeType = Mimer().get(entry.fileName);
var mimeGroup = mimeType.split('/')[0];
var path = sanitize($scope.cwd + '/' + entry.fileName);
if (mimeGroup === 'video' || mimeGroup === 'image') {
$scope.mediaViewer.show(entry);
} else if (mimeType === 'application/pdf') {
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
} else if (mimeGroup === 'text' || mimeGroup === 'application') {
$scope.$parent.textEditor.show($scope.cwd, entry);
} else {
Client.filesGet($scope.backendId, $scope.backendType, path, 'open', function (error) { if (error) return Client.error(error); });
}
$scope.busy = false;
}
$scope.open = function (entry) {
if (entry.isDirectory) openDirectory(sanitize($scope.cwd + '/' + entry.fileName));
else if (entry.isFile) openFile(entry);
};
$scope.goDirectoryUp = function () {
openDirectory(sanitize($scope.cwd + '/..'));
};
$scope.changeDirectory = function (path) {
openDirectory(sanitize(path));
};
$scope.onClearSelection = function ($event) {
// we don't stop propagation if targets don't match we got the whole list click event
if ($event.currentTarget !== $event.target) return;
$scope.selected = [];
};
$scope.onMousedown = function ($event, entry) {
if ($event.button === 2) {
$scope.onMouseup($event, entry);
}
};
$scope.onMouseup = function ($event, entry) {
var i = $scope.selected.indexOf(entry);
var multi = ($event.ctrlKey || $event.metaKey);
var shift = $event.shiftKey;
if (shift) {
if ($scope.selected.length === 0) {
$scope.selected = [ entry ];
} else {
var pos = $scope.entries.indexOf(entry);
var selectedPositions = $scope.selected.map(function (s) { return $scope.entries.indexOf(s); }).sort();
if (pos < selectedPositions[0]) {
$scope.selected = $scope.entries.slice(pos, selectedPositions[0]+1);
} else if (selectedPositions[1] && pos > selectedPositions[1]) {
$scope.selected = $scope.entries.slice(selectedPositions[1], pos+1);
} else {
$scope.selected = $scope.entries.slice(selectedPositions[0], pos+1);
}
}
} else if (multi) {
if (i === -1) {
$scope.selected.push(entry);
} else if ($event.button === 0) { // only do this on left click
$scope.selected.splice(i, 1);
}
} else {
$scope.selected = [ entry ];
}
};
$scope.onEntryContextMenu = function ($event, entry) {
if ($scope.selected.indexOf(entry) !== -1) return;
$scope.selected.push(entry);
};
$scope.actionSelectAll = function () {
$scope.selected = $scope.entries.slice();
};
// just events to the parent controller
$scope.onUploadFile = function () { ctrl.onUploadFile({ cwd: $scope.cwd }); };
$scope.onUploadFolder = function () { ctrl.onUploadFolder({ cwd: $scope.cwd }); };
$scope.onNewFile = function () { ctrl.onNewFile({ cwd: $scope.cwd }); };
$scope.onNewFolder = function () { ctrl.onNewFolder({ cwd: $scope.cwd }); };
$scope.mediaViewer = {
type: '',
src: '',
entry: null,
show: function (entry) {
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
$scope.mediaViewer.entry = entry;
$scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0];
$scope.mediaViewer.src = Client.filesGetLink($scope.backendId, $scope.backendType, filePath);
$('#mediaViewerModal-' + $scope.$id).modal('show');
},
close: function () {
// set an empty pixel image to bust the cached img to avoid flickering on slow load
$scope.mediaViewer.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==';
$('#mediaViewerModal-' + $scope.$id).modal('hide');
}
};
$translate(['filemanager.list.menu.edit', 'filemanager.list.menu.cut', 'filemanager.list.menu.copy', 'filemanager.list.menu.paste', 'filemanager.list.menu.rename', 'filemanager.list.menu.chown', 'filemanager.list.menu.extract', 'filemanager.list.menu.download', 'filemanager.list.menu.delete' ]).then(function (tr) {
$scope.menuOptions = [
{
text: tr['filemanager.list.menu.edit'],
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && !entry.isSymbolicLink; },
enabled: function () { return $scope.selected.length === 1; },
hasBottomDivider: true,
click: function ($itemScope, $event, entry) { $scope.open(entry); }
}, {
text: tr['filemanager.list.menu.cut'],
click: function ($itemScope, $event, entry) { ctrl.onCutEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
}, {
text: tr['filemanager.list.menu.copy'],
click: function ($itemScope, $event, entry) { ctrl.onCopyEntries({ cwd: $scope.cwd, entries: $scope.selected.slice() }); }
}, {
text: tr['filemanager.list.menu.paste'],
hasBottomDivider: true,
enabled: function () { return ctrl.clipboard.length; },
click: function ($itemScope, $event, entry) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: entry }); }
}, {
text: tr['filemanager.list.menu.rename'],
enabled: function () { return $scope.selected.length === 1; },
click: function ($itemScope, $event, entry) { ctrl.onRenameEntry({ cwd: $scope.cwd, entry: entry }); }
}, {
text: tr['filemanager.list.menu.chown'],
click: function ($itemScope, $event, entry) { ctrl.onChownEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
}, {
text: tr['filemanager.list.menu.extract'],
displayed: function ($itemScope, $event, entry) { return !entry.isDirectory && isArchive(entry.fileName); },
click: function ($itemScope, $event, entry) { ctrl.onExtractEntry({ cwd: $scope.cwd, entry: entry }); }
}, {
text: tr['filemanager.list.menu.download'],
enabled: function () { return $scope.selected.length === 1; },
click: function ($itemScope, $event, entry) { download(entry); }
}, {
text: tr['filemanager.list.menu.delete'],
hasTopDivider: true,
click: function ($itemScope, $event, entry) { ctrl.onDeleteEntries({ cwd: $scope.cwd, entries: $scope.selected }); }
}
];
});
$translate(['filemanager.toolbar.newFile', 'filemanager.toolbar.newFolder', 'filemanager.list.menu.paste', 'filemanager.list.menu.selectAll' ]).then(function (tr) {
$scope.menuOptionsBlank = [
{
text: tr['filemanager.toolbar.newFile'],
click: function ($itemScope, $event) { ctrl.onNewFile({ cwd: $scope.cwd }); }
}, {
text: tr['filemanager.toolbar.newFolder'],
click: function ($itemScope, $event) { ctrl.onNewFolder({ cwd: $scope.cwd }); }
}, {
text: tr['filemanager.list.menu.paste'],
hasTopDivider: true,
hasBottomDivider: true,
enabled: function () { return ctrl.clipboard.length; },
click: function ($itemScope, $event) { ctrl.onPasteEntries({ cwd: $scope.cwd, entry: null }); }
}, {
text: tr['filemanager.list.menu.selectAll'],
click: function ($itemScope, $event) { $scope.actionSelectAll(); }
}
];
});
function scrollInView(element) {
if (!element) return;
// This assumes the DOM tree being that rigid
function isVisible(ele) {
var container = ele.parentElement.parentElement.parentElement;
var eleTop = ele.offsetTop;
var eleBottom = eleTop + ele.clientHeight;
var containerTop = container.scrollTop;
var containerBottom = containerTop + container.clientHeight;
// The element is fully visible in the container
return (
(eleTop >= containerTop && eleBottom <= containerBottom) ||
// Some part of the element is visible in the container
(eleTop < containerTop && containerTop < eleBottom) ||
(eleTop < containerBottom && containerBottom < eleBottom)
);
}
if (!isVisible(element)) element.scrollIntoView();
}
function openSelected() {
if (!$scope.selected.length) return;
$scope.open($scope.selected[0]);
}
function selectNext() {
var entries = sort();
if (!$scope.selected.length) return $scope.selected = [ entries[0] ];
var curIndex = $scope.entries.indexOf($scope.selected[0]);
if (curIndex !== -1 && curIndex < $scope.entries.length-1) {
var entry = entries[++curIndex];
$scope.selected = [ entry ];
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
}
}
function selectPrev() {
var entries = sort();
if (!$scope.selected.length) return $scope.selected = [ entries.slice(-1) ];
var curIndex = $scope.entries.indexOf($scope.selected[0]);
if (curIndex !== -1 && curIndex !== 0) {
var entry = entries[--curIndex];
$scope.selected = [ entry ];
scrollInView(document.querySelector('[entry-hashkey="' + entry['$$hashKey'] + '"]'));
}
}
openDirectory('.');
$('.file-list').on('scroll', function (event) {
if (event.target.scrollTop > 10) event.target.classList.add('top-scroll-indicator');
else event.target.classList.remove('top-scroll-indicator');
});
// handle shortcuts
window.addEventListener('keydown', function (event) {
if ($scope.$parent.activeView !== $scope.view || $scope.$parent.viewerOpen || isModalVisible()) return;
if (event.key === 'ArrowDown') {
$scope.$apply(selectNext);
} else if (event.key === 'ArrowUp') {
$scope.$apply(selectPrev);
} else if (event.key === 'Enter') {
$scope.$apply(openSelected);
} else if (event.key === 'Backspace') {
if ($scope.view === 'fileTree') $scope.goDirectoryUp();
}
});
}
-354
View File
@@ -1,354 +0,0 @@
<!DOCTYPE html>
<html ng-app="Application">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title>Cloudron Filemanager</title>
<meta name="description" content="Cloudron Filemanager">
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
<link rel="icon" href="/api/v1/cloudron/avatar">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Fontawesome -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
<!-- async -->
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
<!-- Showdown (markdown converter) -->
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
<!-- Angular translate https://angular-translate.github.io/ -->
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
<!-- colors -->
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
<!-- https://github.com/data-uri/mimer -->
<script type="text/javascript" src="/3rdparty/js/mimer.min.js?<%= revision %>"></script>
<!-- https://github.com/Templarian/ui.bootstrap.contextMenu -->
<script type="text/javascript" src="/3rdparty/js/contextMenu.js?<%= revision %>"></script>
<!-- WARNING this adds an AMD loader! Make sure script tag includes like mimer are above -->
<!-- monaco-editor -->
<script type="text/javascript" src="/3rdparty/vs/loader.js?<%= revision %>"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/filemanager.js?<%= revision %>"></script>
</head>
<body class="filemanager" ng-drop="drop($event)" ng-dragover="dragEnter($event)" ng-dragleave="dragExit($event)" ng-controller="FileManagerController">
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
<div class="restart-banner animateMe" ng-show="restartBusy" ng-cloak><i class="fa fa-circle-notch fa-spin"></i> {{ 'filemanager.status.restartingApp' | tr}}</div>
<!-- Modal delete entries -->
<div class="modal fade" id="entriesDeleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<p class="text-bold text-danger" ng-show="deleteEntries.error">{{ deleteEntries.error }}</p>
<h4 ng-hide="deleteEntries.error">{{ 'filemanager.removeDialog.reallyDelete' | tr }}</h4>
<ul>
<li ng-repeat="entry in deleteEntries.entries">{{ entry.fileName }}</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.no' | tr }}</button>
<button type="button" class="btn btn-danger" ng-click="deleteEntries.submit()" ng-hide="deleteEntries.error" ng-disabled="deleteEntries.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteEntries.busy"></i> {{ 'main.dialog.yes' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal rename entry -->
<div class="modal fade" id="renameEntryModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.renameDialog.title' | tr:{ fileName: renameEntry.entry.fileName } }}</h4>
</div>
<div class="modal-body">
<form role="form" name="renameEntryForm" ng-submit="renameEntry.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (renameEntryForm.newName.$dirty && renameEntryForm.newName.$invalid) }">
<label class="control-label">{{ 'filemanager.renameDialog.newName' | tr }}</label>
<div class="control-label" ng-show="renameEntry.error">{{ renameEntry.error }}</div>
<input type="text" class="form-control" id="inputNewName" name="newName" ng-model="renameEntry.newName" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="renameEntryForm.$invalid || renameEntry.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="renameEntry.submit()" ng-hide="renameEntry.error" ng-disabled="renameEntry.busy"><i class="fa fa-circle-notch fa-spin" ng-show="renameEntry.busy"></i> {{ 'filemanager.renameDialog.rename' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal extract -->
<div class="modal fade" id="extractModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.extractDialog.title' | tr:{ fileName: extractStatus.fileName } }}</h4>
</div>
<div class="modal-body">
<div ng-show="extractStatus.error">
<p class="text-danger">{{ extractStatus.error }}</p>
</div>
<div class="progress progress-striped active" ng-hide="extractStatus.error">
<div class="progress-bar" role="progressbar" style="width: 100%">
</div>
</div>
<p class="no-wrap" ng-hide="extractStatus.error">{{ extractStatus.fileName }}</p>
</div>
<div class="modal-footer" style="text-align: left;">
<small ng-hide="extractStatus.error">{{ 'filemanager.extractDialog.closeWarning' | tr }}</small>
<button class="btn btn-primary pull-right" ng-show="extractStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal chown entry -->
<div class="modal fade" id="chownEntriesModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.chownDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<form role="form" name="chownEntryForm" ng-submit="chownEntries.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (chownEntryForm.newOwner.$dirty && chownEntries.error) }">
<label class="control-label">{{ 'filemanager.chownDialog.newOwner' | tr }}</label>
<div class="control-label" for="inputNewOwner" ng-show="chownEntries.error">{{ chownEntries.error }}</div>
<select class="form-control" id="inputNewOwner" name="newOwner" ng-model="chownEntries.newOwner" ng-options="a.value as a.name for a in OWNERS" ng-disabled="chownEntries.busy"></select>
</div>
<div class="form-group" ng-show="chownEntries.showRecursiveOption">
<input type="checkbox" id="inputNewOwnerRecursive" ng-model="chownEntries.recursive">
<label class="control-label" for="inputNewOwnerRecursive">{{ 'filemanager.chownDialog.recursiveCheckbox' | tr }}</label>
</div>
<input class="ng-hide" type="submit" ng-disabled="chownEntryForm.$invalid || chownEntries.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="chownEntries.submit()" ng-hide="chownEntries.error" ng-disabled="chownEntries.busy"><i class="fa fa-circle-notch fa-spin" ng-show="chownEntries.busy"></i> {{ 'filemanager.chownDialog.change' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal new file -->
<div class="modal fade" id="newFileModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.newFileDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<form role="form" name="newFileForm" ng-submit="newFile.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': newFile.error || (newFileForm.fileName.$dirty && newFileForm.fileName.$invalid) }">
<input type="text" class="form-control" id="inputFileName" name="fileName" ng-model="newFile.name" required autofocus>
<div class="control-label" ng-show="newFile.error === 'exists'">{{ 'filemanager.newFile.errorAlreadyExists' | tr }}</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="newFileForm.$invalid || newFile.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="newFile.submit()" ng-disabled="newFile.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newFile.busy"></i> {{ 'filemanager.newFileDialog.create' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal new directory -->
<div class="modal fade" id="newFolderModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.newDirectoryDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<form role="form" name="newFolderForm" ng-submit="newFolder.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': newFolder.error || (newFolderForm.directoryName.$dirty && newFolderForm.directoryName.$invalid) }">
<input type="text" class="form-control" id="inputDirectoryName" name="directoryName" ng-model="newFolder.name" required autofocus>
<div class="control-label" ng-show="newFolder.error === 'exists'">{{ 'filemanager.newDirectory.errorAlreadyExists' | tr }}</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="newDirectoryForm.$invalid || newFolder.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="newFolder.submit()" ng-disabled="newFolder.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newFolder.busy"></i> {{ 'filemanager.newDirectoryDialog.create' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal editor close -->
<div class="modal fade" id="textEditorCloseModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.textEditorCloseDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<p class="text-bold text-danger">{{ 'filemanager.textEditorCloseDialog.details' | tr }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="textEditor.onClose()">{{ 'filemanager.textEditorCloseDialog.dontSave' | tr }}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="textEditor.saveAndClose()"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal upload -->
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'filemanager.uploadingDialog.title' | tr:{ countDone: uploadStatus.countDone, count: uploadStatus.count } }}</h4>
</div>
<div class="modal-body">
<div ng-show="uploadStatus.error">
<p class="text-danger" ng-show="uploadStatus.error === 'exists'">{{ 'filemanager.uploadingDialog.errorAlreadyExists' | tr }}</p>
<p class="text-danger" ng-show="uploadStatus.error === 'generic'">{{ 'filemanager.uploadingDialog.errorFailed' | tr }}</p>
</div>
<span><b>{{ uploadStatus.sizeDone | prettyDecimalSize }}</b> (total {{ uploadStatus.size | prettyDecimalSize }})</span>
<div class="progress progress-striped active" ng-hide="uploadStatus.error">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ uploadStatus.percentDone || 0 }}%"></div>
</div>
<p class="no-wrap" ng-hide="uploadStatus.error">{{ uploadStatus.fileName }}</p>
</div>
<div class="modal-footer" style="text-align: left;">
<small ng-hide="uploadStatus.error">{{ 'filemanager.uploadingDialog.closeWarning' | tr }}</small>
<button class="btn btn-default pull-right" ng-show="uploadStatus.error" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button class="btn btn-primary pull-right" ng-show="uploadStatus.error === 'generic'" ng-click="retryUpload(false)">{{ 'filemanager.uploadingDialog.retry' | tr }}</button>
<button class="btn btn-danger pull-right" ng-show="uploadStatus.error === 'exists'" ng-click="retryUpload(true)">{{ 'filemanager.uploadingDialog.overwrite' | tr }}</button>
</div>
</div>
</div>
</div>
<div class="animateMe ng-hide layout-root" ng-show="initialized">
<div class="row" ng-hide="title">
<div class="col-md-12 text-center">
<h3>{{ 'filemanager.notFound' | tr }}</h3>
</div>
</div>
<input type="file" id="uploadFileInput" style="display: none" multiple/>
<input type="file" id="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
<div class="container card" ng-show="title" style="max-width: unset;">
<h4 class="text-left">
{{ title }}
<div class="pull-right">
<div class="btn-group" ng-show="volumes.length">
<button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><i class="fas fa-folder"></i> <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li ng-repeat="volume in volumes"><a class="hand" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank"><i class="fas fa-folder fa-fw"></i> {{ volume.name }}</a></li>
</ul>
</div>
<button type="button" class="btn btn-sm btn-default" ng-class="{ 'active': splitView }" ng-click="toggleSplitView()"><i class="fas fa-columns"></i></button>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-click="onRestartApp()" uib-tooltip="{{ 'filemanager.toolbar.restartApp' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-sync-alt"></i></button>
<button type="button" class="btn btn-sm btn-default" ng-show="backendType === 'mail'" ng-click="onRestartMail()" uib-tooltip="{{ 'filemanager.toolbar.restartApp' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-sync-alt"></i></button>
<a type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-href="/logs.html?{{ backendType === 'app' ? 'appId=' + backendId : 'id=mail' }}" target="_blank" uib-tooltip="{{ 'filemanager.toolbar.openLogs' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fas fa-align-left"></i></a>
<a type="button" class="btn btn-sm btn-default" ng-show="backendType === 'app'" ng-href="{{ '/terminal.html?id=' + backendId }}" target="_blank" uib-tooltip="{{ 'filemanager.toolbar.openTerminal' | tr }}" tooltip-placement="bottom" tooltip-append-to-body="true"><i class="fa fa-terminal"></i></a>
</div>
</div>
</h4>
<div class="file-trees">
<filetree ng-class="{ 'two-pane': splitView }"
on-upload-folder="onUploadFolder(cwd)"
on-upload-file="onUploadFile(cwd)"
on-new-file="newFile.show(cwd)"
on-new-folder="newFolder.show(cwd)"
on-copy-entries="actionCopy(cwd, entries)"
on-cut-entries="actionCut(cwd, entries)"
on-paste-entries="actionPaste(cwd, entries)"
on-delete-entries="deleteEntries.show(cwd, entries)"
on-rename-entry="renameEntry.show(cwd, entry)"
on-extract-entry="extractEntry(cwd, entry)"
on-chown-entries="chownEntries.show(cwd, entries)"
backend-type="backendType" backend-id="backendId" view="VIEW.LEFT" clipboard="clipboard"
ng-click="setActiveView(VIEW.LEFT)"></filetree>
<filetree ng-show="splitView" class="two-pane"
on-upload-folder="onUploadFolder(cwd)"
on-upload-file="onUploadFile(cwd)"
on-new-file="newFile.show(cwd)"
on-new-folder="newFolder.show(cwd)"
on-copy-entries="actionCopy(cwd, entries)"
on-cut-entries="actionCut(cwd, entries)"
on-paste-entries="actionPaste(cwd, entries)"
on-delete-entries="deleteEntries.show(cwd, entries)"
on-rename-entry="renameEntry.show(cwd, entry)"
on-extract-entry="extractEntry(cwd, entry)"
on-chown-entries="chownEntries.show(cwd, entries)"
backend-type="backendType" backend-id="backendId" view="VIEW.RIGHT" clipboard="clipboard"
ng-click="setActiveView(VIEW.RIGHT)"></filetree>
</div>
</div>
</div>
<div ng-show="textEditor.visible" class="text-editor">
<div>
<div class="toolbar">
<div><span>{{ textEditor.entry.fileName }}</span></div>
<button type="button" class="btn btn-primary" ng-click="textEditor.maybeClose()">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="textEditor.save()" ng-disabled="textEditor.busy"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
<div id="textEditorContainer" style="flex-grow: 2; border: 0px solid black"></div>
</div>
<footer class="text-center ng-cloak">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
</footer>
</div>
</body>
</html>
+2 -4
View File
@@ -80,9 +80,6 @@
<!-- Anugular Multiselect https://github.com/sebastianha/angular-bootstrap-multiselect -->
<script type="text/javascript" src="/3rdparty/js/angular-bootstrap-multiselect.js?<%= revision %>"></script>
<!-- colors -->
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
@@ -159,7 +156,7 @@
</a>
</li>
<li>
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-th fa-fw"></i> {{ 'apps.title' | tr }}</a>
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-grip fa-fw"></i> {{ 'apps.title' | tr }}</a>
</li>
<li ng-show="user.isAtLeastAdmin">
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-cloud-download-alt fa-fw"></i> {{ 'appstore.title' | tr }}</a>
@@ -187,6 +184,7 @@
<li ng-show="user.isAtLeastAdmin"><a href="#/network"><i class="fas fa-network-wired fa-fw"></i> {{ 'network.title' | tr }}</a></li>
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> {{ 'services.title' | tr }}</a></li>
<li ng-show="user.isAtLeastAdmin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> {{ 'settings.title' | tr }}</a></li>
<li ng-show="user.isAtLeastAdmin"><a href="#/usersettings"><i class="fa fa-users-gear fa-fw"></i> {{ 'users.title' | tr }}</a></li>
<li ng-show="user.isAtLeastAdmin"><a href="#/volumes"><i class="fa fa-hdd fa-fw"></i> {{ 'volumes.title' | tr }}</a></li>
<li ng-show="user.isAtLeastAdmin" class="divider"></li>
<li ng-show="user.isAtLeastOwner"><a href="#/support"><i class="fa fa-comment fa-fw"></i> {{ 'support.title' | tr }}</a></li>
+166 -383
View File
File diff suppressed because it is too large Load Diff
-890
View File
@@ -1,890 +0,0 @@
'use strict';
require.config({ paths: { 'vs': '3rdparty/vs' }});
// create main application module
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ngDrag', 'ui.bootstrap', 'ui.bootstrap.contextMenu']);
angular.module('Application').filter('prettyOwner', function () {
return function (uid) {
if (uid === 0) return 'root';
if (uid === 33) return 'www-data';
if (uid === 1000) return 'cloudron';
if (uid === 1001) return 'git';
return uid;
};
});
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
app.config(function ($sceProvider) {
$sceProvider.enabled(false);
});
app.filter('trustUrl', ['$sce', function ($sce) {
return function (recordingUrl) {
return $sce.trustAsResourceUrl(recordingUrl);
};
}]);
// https://stackoverflow.com/questions/25621321/angularjs-ng-drag
var ngDragEventDirectives = {};
angular.forEach(
'drag dragend dragenter dragexit dragleave dragover dragstart drop'.split(' '),
function(eventName) {
var directiveName = 'ng' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
ngDragEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse/*, $rootScope */) {
return {
restrict: 'A',
compile: function($element, attr) {
var fn = $parse(attr[directiveName], null, true);
return function ngDragEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
scope.$apply(callback);
});
};
}
};
}];
}
);
angular.module('ngDrag', []).directive(ngDragEventDirectives);
function sanitize(filePath) {
filePath = filePath.split('/').filter(function (a) { return !!a; }).reduce(function (a, v) {
if (v === '.'); // do nothing
else if (v === '..') a.pop();
else a.push(v);
return a;
}, []).map(function (p) {
// small detour to safely handle special characters and whitespace
return encodeURIComponent(decodeURIComponent(p));
}).join('/');
return filePath;
}
function isModalVisible() {
return !!document.getElementsByClassName('modal in').length;
}
var VIEW = {
LEFT: 'left',
RIGHT: 'right'
};
var OWNERS = [
{ name: 'cloudron', value: 1000 },
{ name: 'www-data', value: 33 },
{ name: 'git', value: 1001 },
{ name: 'root', value: 0 }
];
app.controller('FileManagerController', ['$scope', '$translate', '$timeout', 'Client', function ($scope, $translate, $timeout, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
// expose enums
$scope.VIEW = VIEW;
$scope.OWNERS = OWNERS;
$scope.initialized = false;
$scope.status = null;
$scope.client = Client;
$scope.title = '';
$scope.backendId = search.id;
$scope.backendType = search.type;
$scope.volumes = [];
$scope.splitView = !!window.localStorage.splitView;
$scope.activeView = VIEW.LEFT;
$scope.viewerOpen = false;
$scope.clipboard = []; // holds cut or copied entries
$scope.clipboardCut = false; // if action is cut or copy
// add a hook for children to refresh both tree views
$scope.children = [];
$scope.registerChild = function (child) { $scope.children.push(child); };
$scope.refresh = function () {
$scope.children.forEach(function (child) {
child.onRefresh();
});
};
function collectFiles(entry, callback) {
var pathFrom = entry.pathFrom;
if (entry.isDirectory) {
Client.filesGet($scope.backendId, $scope.backendType, entry.fullFilePath, 'data', function (error, result) {
if (error) return callback(error);
if (!result.entries) return callback(new Error('not a folder'));
// amend fullFilePath
result.entries.forEach(function (e) {
e.fullFilePath = sanitize(entry.fullFilePath + '/' + e.fileName);
e.pathFrom = pathFrom; // we stash the original path for pasting
});
var collectedFiles = [];
async.eachLimit(result.entries, 5, function (entry, callback) {
collectFiles(entry, function (error, result) {
if (error) return callback(error);
collectedFiles = collectedFiles.concat(result);
callback();
});
}, function (error) {
if (error) return callback(error);
callback(null, collectedFiles);
});
});
return;
}
callback(null, [ entry ]);
}
// entries need to be an actual copy
$scope.actionCut = function (cwd, entries) {
$scope.clipboard = entries; //$scope.selected.slice();
$scope.clipboard.forEach(function (entry) {
entry.fullFilePath = sanitize(cwd + '/' + entry.fileName);
});
$scope.clipboardCut = true;
};
// entries need to be an actual copy
$scope.actionCopy = function (cwd, entries) {
$scope.clipboard = entries; //$scope.selected.slice();
$scope.clipboard.forEach(function (entry) {
entry.fullFilePath = sanitize(cwd + '/' + entry.fileName);
entry.pathFrom = cwd; // we stash the original path for pasting
});
$scope.clipboardCut = false;
};
$scope.actionPaste = function (cwd, destinationEntry) {
if ($scope.clipboardCut) {
// move files
async.eachLimit($scope.clipboard, 5, function (entry, callback) {
var newFilePath = sanitize(cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fileName);
// TODO this will overwrite files in destination!
Client.filesRename($scope.backendId, $scope.backendType, entry.fullFilePath, newFilePath, callback);
}, function (error) {
if (error) return Client.error(error);
// clear clipboard
$scope.clipboard = [];
$scope.refresh();
});
} else {
// copy files
// first collect all files recursively
var collectedFiles = [];
async.eachLimit($scope.clipboard, 5, function (entry, callback) {
collectFiles(entry, function (error, result) {
if (error) return callback(error);
collectedFiles = collectedFiles.concat(result);
callback();
});
}, function (error) {
if (error) return Client.error(error);
async.eachLimit(collectedFiles, 5, function (entry, callback) {
var newFilePath = sanitize(cwd + '/' + ((destinationEntry && destinationEntry.isDirectory) ? destinationEntry.fileName : '') + '/' + entry.fullFilePath.slice(entry.pathFrom.length));
// This will NOT overwrite but finds a unique new name to copy to
// we prefix with a / to ensure we don't do relative target paths
Client.filesCopy($scope.backendId, $scope.backendType, entry.fullFilePath, '/' + newFilePath, callback);
}, function (error) {
if (error) return Client.error(error);
// clear clipboard
$scope.clipboard = [];
$scope.refresh();
});
});
}
};
// handle uploads
$scope.uploadStatus = {
error: null,
busy: false,
fileName: '',
count: 0,
countDone: 0,
size: 0,
done: 0,
percentDone: 0,
files: [],
targetFolder: ''
};
$scope.uploadFiles = function (files, targetFolder, overwrite) {
if (!files || !files.length) return;
overwrite = !!overwrite;
// prevent it from getting closed
$('#uploadModal').modal({
backdrop: 'static',
keyboard: false
});
$scope.uploadStatus.files = files;
$scope.uploadStatus.targetFolder = targetFolder;
$scope.uploadStatus.error = null;
$scope.uploadStatus.busy = true;
$scope.uploadStatus.count = files.length;
$scope.uploadStatus.countDone = 0;
$scope.uploadStatus.size = 0;
$scope.uploadStatus.sizeDone = 0;
$scope.uploadStatus.done = 0;
$scope.uploadStatus.percentDone = 0;
for (var i = 0; i < files.length; ++i) {
$scope.uploadStatus.size += files[i].size;
}
async.eachSeries(files, function (file, callback) {
var filePath = sanitize(targetFolder + '/' + (file.webkitRelativePath || file.name));
$scope.uploadStatus.fileName = file.name;
Client.filesUpload($scope.backendId, $scope.backendType, filePath, file, overwrite, function (loaded) {
$scope.uploadStatus.percentDone = ($scope.uploadStatus.done+loaded) * 100 / $scope.uploadStatus.size;
$scope.uploadStatus.sizeDone = loaded;
}, function (error) {
if (error) return callback(error);
$scope.uploadStatus.done += file.size;
$scope.uploadStatus.percentDone = $scope.uploadStatus.done * 100 / $scope.uploadStatus.size;
$scope.uploadStatus.countDone++;
callback();
});
}, function (error) {
$scope.uploadStatus.busy = false;
if (error && error.statusCode === 409) {
$scope.uploadStatus.error = 'exists';
return;
} else if (error) {
console.error(error);
$scope.uploadStatus.error = 'generic';
return;
}
$('#uploadModal').modal('hide');
$scope.uploadStatus.fileName = '';
$scope.uploadStatus.count = 0;
$scope.uploadStatus.size = 0;
$scope.uploadStatus.sizeDone = 0;
$scope.uploadStatus.done = 0;
$scope.uploadStatus.percentDone = 100;
$scope.uploadStatus.files = [];
$scope.uploadStatus.targetFolder = '';
$scope.refresh();
});
};
$scope.retryUpload = function (overwrite) {
$scope.uploadFiles($scope.uploadStatus.files, $scope.uploadStatus.targetFolder, !!overwrite);
};
// file and folder upload hooks, stashing $scope.uploadCwd for now
$scope.uploadCwd = '';
$('#uploadFileInput').on('change', function (e ) {
$scope.uploadFiles(e.target.files || [], $scope.uploadCwd, false);
});
$scope.onUploadFile = function (cwd) {
$scope.uploadCwd = cwd;
$('#uploadFileInput').click();
};
$('#uploadFolderInput').on('change', function (e ) {
$scope.uploadFiles(e.target.files || [], $scope.uploadCwd, false);
});
$scope.onUploadFolder = function (cwd) {
$scope.uploadCwd = cwd;
$('#uploadFolderInput').click();
};
// handle delete
$scope.deleteEntries = {
busy: false,
error: null,
cwd: '',
entries: [],
show: function (cwd, entries) {
$scope.deleteEntries.error = null;
$scope.deleteEntries.cwd = cwd;
$scope.deleteEntries.entries = entries;
$('#entriesDeleteModal').modal('show');
},
submit: function () {
$scope.deleteEntries.busy = true;
async.eachLimit($scope.deleteEntries.entries, 5, function (entry, callback) {
var filePath = sanitize($scope.deleteEntries.cwd + '/' + entry.fileName);
Client.filesRemove($scope.backendId, $scope.backendType, filePath, callback);
}, function (error) {
$scope.deleteEntries.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#entriesDeleteModal').modal('hide');
});
}
};
// rename entry
$scope.renameEntry = {
busy: false,
error: null,
entry: null,
cwd: '',
newName: '',
show: function (cwd, entry) {
$scope.renameEntry.error = null;
$scope.renameEntry.cwd = cwd;
$scope.renameEntry.entry = entry;
$scope.renameEntry.newName = entry.fileName;
$scope.renameEntry.busy = false;
$('#renameEntryModal').modal('show');
},
submit: function () {
$scope.renameEntry.busy = true;
var oldFilePath = sanitize($scope.renameEntry.cwd + '/' + $scope.renameEntry.entry.fileName);
var newFilePath = sanitize(($scope.renameEntry.newName[0] === '/' ? '' : ($scope.renameEntry.cwd + '/')) + $scope.renameEntry.newName);
Client.filesRename($scope.backendId, $scope.backendType, oldFilePath, newFilePath, function (error) {
$scope.renameEntry.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#renameEntryModal').modal('hide');
});
}
};
// chown entries
$scope.chownEntries = {
busy: false,
error: null,
entries: [],
newOwner: 0,
recursive: false,
showRecursiveOption: false,
show: function (cwd, entries) {
$scope.chownEntries.error = null;
$scope.chownEntries.cwd = cwd;
$scope.chownEntries.entries = entries;
// set default uid from first file
$scope.chownEntries.newOwner = entries[0].uid;
$scope.chownEntries.busy = false;
// default for directories is recursive
$scope.chownEntries.recursive = !!entries.find(function (entry) { return entry.isDirectory; });
$scope.chownEntries.showRecursiveOption = false;
$('#chownEntriesModal').modal('show');
},
submit: function () {
$scope.chownEntries.busy = true;
async.eachLimit($scope.chownEntries.entries, 5, function (entry, callback) {
var filePath = sanitize($scope.chownEntries.cwd + '/' + entry.fileName);
Client.filesChown($scope.backendId, $scope.backendType, filePath, $scope.chownEntries.newOwner, $scope.chownEntries.recursive, callback);
}, function (error) {
$scope.chownEntries.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#chownEntriesModal').modal('hide');
});
}
};
// new file
$scope.newFile = {
busy: false,
error: null,
cwd: '',
name: '',
show: function (cwd) {
$scope.newFile.error = null;
$scope.newFile.name = '';
$scope.newFile.busy = false;
$scope.newFile.cwd = cwd;
$scope.newFileForm.$setUntouched();
$scope.newFileForm.$setPristine();
$('#newFileModal').modal('show');
},
submit: function () {
$scope.newFile.busy = true;
$scope.newFile.error = null;
var filePath = sanitize($scope.newFile.cwd + '/' + $scope.newFile.name);
Client.filesUpload($scope.backendId, $scope.backendType, filePath, new File([], $scope.newFile.name), false, function () {}, function (error) {
$scope.newFile.busy = false;
if (error && error.statusCode === 409) return $scope.newFile.error = 'exists';
if (error) return Client.error(error);
$scope.refresh();
$('#newFileModal').modal('hide');
});
}
};
// new folder
$scope.newFolder = {
busy: false,
error: null,
cwd: '',
name: '',
show: function (cwd) {
$scope.newFolder.error = null;
$scope.newFolder.name = '';
$scope.newFolder.busy = false;
$scope.newFolder.cwd = cwd;
$scope.newFolderForm.$setUntouched();
$scope.newFolderForm.$setPristine();
$('#newFolderModal').modal('show');
},
submit: function () {
$scope.newFolder.busy = true;
$scope.newFolder.error = null;
var filePath = sanitize($scope.newFolder.cwd + '/' + $scope.newFolder.name);
Client.filesCreateDirectory($scope.backendId, $scope.backendType, filePath, function (error) {
$scope.newFolder.busy = false;
if (error && error.statusCode === 409) return $scope.newFolder.error = 'exists';
if (error) return Client.error(error);
$scope.refresh();
$('#newFolderModal').modal('hide');
});
}
};
// extract archives
$scope.extractStatus = {
error: null,
busy: false,
fileName: ''
};
$scope.extractEntry = function (cwd, entry) {
var filePath = sanitize(cwd + '/' + entry.fileName);
if (entry.isDirectory) return;
// prevent it from getting closed
$('#extractModal').modal({
backdrop: 'static',
keyboard: false
});
$scope.extractStatus.fileName = entry.fileName;
$scope.extractStatus.error = null;
$scope.extractStatus.busy = true;
Client.filesExtract($scope.backendId, $scope.backendType, filePath, function (error) {
$scope.extractStatus.busy = false;
if (error) {
console.error(error);
$scope.extractStatus.error = $translate.instant('filemanager.extract.error', error.message);
return;
}
$('#extractModal').modal('hide');
$scope.refresh();
});
};
// split view handling
$scope.toggleSplitView = function () {
$scope.splitView = !$scope.splitView;
if (!$scope.splitView) {
$scope.activeView = VIEW.LEFT;
delete window.localStorage.splitView;
} else {
window.localStorage.splitView = true;
}
};
$scope.setActiveView = function (view) {
$scope.activeView = view;
};
// monaco text editor
var LANGUAGES = [];
require(['vs/editor/editor.main'], function() { LANGUAGES = monaco.languages.getLanguages(); });
function getLanguage(filename) {
var ext = '.' + filename.split('.').pop();
var language = LANGUAGES.find(function (l) {
if (!l.extensions) return false;
return !!l.extensions.find(function (e) { return e === ext; });
}) || '';
return language ? language.id : '';
}
$scope.textEditor = {
busy: false,
cwd: null,
entry: null,
editor: null,
unsaved: false,
visible: false,
show: function (cwd, entry) {
$scope.textEditor.cwd = cwd;
$scope.textEditor.entry = entry;
$scope.textEditor.busy = false;
$scope.textEditor.unsaved = false;
$scope.textEditor.visible = true;
// clear model if any
if ($scope.textEditor.editor && $scope.textEditor.editor.getModel()) $scope.textEditor.editor.setModel(null);
$scope.viewerOpen = true;
// document.getElementById('textEditorModal').style['display'] = 'flex';
var filePath = sanitize($scope.textEditor.cwd + '/' + entry.fileName);
var language = getLanguage(entry.fileName);
Client.filesGet($scope.backendId, $scope.backendType, filePath, 'data', function (error, result) {
if (error) return Client.error(error);
if (!$scope.textEditor.editor) {
$timeout(function () {
$scope.textEditor.editor = monaco.editor.create(document.getElementById('textEditorContainer'), {
value: result,
language: language,
theme: 'vs-dark'
});
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; });
}, 200);
} else {
$scope.textEditor.editor.setModel(monaco.editor.createModel(result, language));
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); // have to re-attach whenever model changes
}
});
},
save: function (callback) {
$scope.textEditor.busy = true;
var newContent = $scope.textEditor.editor.getValue();
var filePath = sanitize($scope.textEditor.cwd + '/' + $scope.textEditor.entry.fileName);
var file = new File([newContent], 'file');
Client.filesUpload($scope.backendId, $scope.backendType, filePath, file, true, function () {}, function (error) {
if (error) return Client.error(error);
$scope.refresh();
$timeout(function () {
$scope.textEditor.unsaved = false;
$scope.textEditor.busy = false;
if (typeof callback === 'function') return callback();
}, 1000);
});
},
close: function () {
$scope.textEditor.visible = false;
$scope.viewerOpen = false;
$('#textEditorCloseModal').modal('hide');
},
onClose: function () {
$scope.textEditor.visible = false;
$scope.viewerOpen = false;
$('#textEditorCloseModal').modal('hide');
},
saveAndClose: function () {
$scope.textEditor.save(function () {
$scope.textEditor.onClose();
});
},
maybeClose: function () {
if (!$scope.textEditor.unsaved) return $scope.textEditor.onClose();
$('#textEditorCloseModal').modal('show');
},
};
// restart app or mail logic
$scope.restartBusy = false;
$scope.onRestartApp = function () {
$scope.restartBusy = true;
function waitUntilRestarted(callback) {
Client.getApp($scope.backendId, function (error, result) {
if (error) return callback(error);
if (result.installationState === ISTATES.INSTALLED) return callback();
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
});
}
Client.restartApp($scope.backendId, function (error) {
if (error) console.error('Failed to restart app.', error);
waitUntilRestarted(function (error) {
if (error) console.error('Failed wait for restart.', error);
$scope.restartBusy = false;
});
});
};
$scope.onRestartMail = function () {
$scope.restartBusy = true;
function waitUntilRestarted(callback) {
Client.getService('mail', function (error, result) {
if (error) return callback(error);
if (result.status === 'active') return callback();
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
});
}
Client.restartService('mail', function (error) {
if (error) console.error('Failed to restart mail.', error);
waitUntilRestarted(function (error) {
if (error) console.error('Failed wait for restart.', error);
$scope.restartBusy = false;
});
});
};
// init code
function fetchVolumesInfo(mounts) {
$scope.volumes = [];
async.each(mounts, function (mount, callback) {
Client.getVolume(mount.volumeId, function (error, result) {
if (error) return callback(error);
$scope.volumes.push(result);
callback();
});
}, function (error) {
if (error) console.error('Failed to fetch volumes info.', error);
});
}
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
$scope.status = status;
console.log('Running filemanager version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
var getter;
if ($scope.backendType === 'app') {
getter = Client.getApp.bind(Client, $scope.backendId);
} else if ($scope.backendType === 'volume') {
getter = Client.getVolume.bind(Client, $scope.backendId);
} else if ($scope.backendType === 'mail') {
getter = function (next) { next(null, null); };
}
getter(function (error, result) {
if (error) {
$scope.initialized = true;
return;
}
// fine to do async
if ($scope.backendType === 'app') fetchVolumesInfo(result.mounts || []);
switch ($scope.backendType) {
case 'app':
$scope.title = result.label || result.fqdn;
$scope.rootDirLabel = '/app/data/';
$scope.applicationLink = 'https://' + result.fqdn;
break;
case 'volume':
$scope.title = result.name;
$scope.rootDirLabel = result.hostPath;
break;
case 'mail':
$scope.title = 'mail';
$scope.rootDirLabel = 'mail';
break;
}
window.document.title = $scope.title + ' - ' + $translate.instant('filemanager.title');
// now mark the Client to be ready
Client.setReady();
// openPath('');
$scope.initialized = true;
});
});
});
}
init();
// toplevel key input handling
window.addEventListener('keydown', function (event) {
if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 's') {
if (!$scope.textEditor.visible) return;
event.preventDefault();
$scope.$apply($scope.textEditor.save);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'c') {
if ($scope.textEditor.visible) return;
if ($scope.selected.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionCopy);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'x') {
if ($scope.textEditor.visible) return;
if ($scope.selected.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionCut);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'v') {
if ($scope.textEditor.visible) return;
if ($scope.clipboard.length === 0) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionPaste);
} else if((navigator.platform.match('Mac') ? event.metaKey : event.ctrlKey) && event.key === 'a') {
if ($scope.textEditor.visible) return;
if (isModalVisible()) return;
event.preventDefault();
$scope.$apply($scope.actionSelectAll);
} else if(event.key === 'Escape') {
if ($scope.textEditor.visible) return $scope.$apply($scope.textEditor.maybeClose);
else $scope.$apply(function () { $scope.selected = []; });
}
});
// setup all the dialog focus handling
['newFileModal', 'newFolderModal', 'renameEntryModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
// selects filename (without extension)
['renameEntryModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
var elem = $(this).find('[autofocus]:first');
var text = elem.val();
elem[0].setSelectionRange(0, text.indexOf('.'));
});
});
}]);
+221 -2
View File
@@ -50,6 +50,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/users', {
controller: 'UsersController',
templateUrl: 'views/users.html?<%= revision %>'
}).when('/usersettings', {
controller: 'UserSettingsController',
templateUrl: 'views/user-settings.html?<%= revision %>'
}).when('/app/:appId/:view?', {
controller: 'AppController',
templateUrl: 'views/app.html?<%= revision %>'
@@ -93,8 +96,7 @@ app.config(['$routeProvider', function ($routeProvider) {
controller: 'NotificationsController',
templateUrl: 'views/notifications.html?<%= revision %>'
}).when('/oidc', {
controller: 'OidcController',
templateUrl: 'views/oidc.html?<%= revision %>'
redirectTo: '/usersettings'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html?<%= revision %>'
@@ -297,6 +299,7 @@ app.filter('installationStateLabel', function () {
case ISTATES.PENDING_LOCATION_CHANGE:
case ISTATES.PENDING_CONFIGURE:
case ISTATES.PENDING_RECREATE_CONTAINER:
case ISTATES.PENDING_SERVICES_CHANGE:
case ISTATES.PENDING_DEBUG:
return 'Configuring' + waiting;
case ISTATES.PENDING_RESIZE:
@@ -607,3 +610,219 @@ app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
max: 24
};
}]);
app.controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
$scope.initialized = false; // used to animate the UI
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.client = Client;
$scope.subscription = {};
$scope.notificationCount = 0;
$scope.hideNavBarActions = $location.path() === '/logs';
$scope.backgroundImageUrl = '';
$scope.reboot = {
busy: false,
show: function () {
$scope.reboot.busy = false;
$('#rebootModal').modal('show');
},
submit: function () {
$scope.reboot.busy = true;
Client.reboot(function (error) {
if (error) return Client.error(error);
$('#rebootModal').modal('hide');
// trigger refetch to show offline banner
$timeout(function () { Client.getStatus(function () {}); }, 5000);
});
}
};
$scope.isActive = function (url) {
if (!$route.current) return false;
return $route.current.$$route.originalPath.indexOf(url) === 0;
};
$scope.logout = function (event) {
event.stopPropagation();
$scope.initialized = false;
Client.logout();
};
$scope.openSubscriptionSetup = function () {
Client.openSubscriptionSetup($scope.subscription);
};
// NOTE: this function is exported and called from the appstore.js
$scope.updateSubscriptionStatus = function () {
Client.getSubscription(function (error, subscription) {
if (error && error.statusCode === 412) return; // not yet registered
if (error && error.statusCode === 402) return; // invalid appstore token
if (error) return console.error(error);
$scope.subscription = subscription;
});
};
function refreshNotifications() {
if (!Client.getUserInfo().isAtLeastAdmin) return;
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
if (error) console.error(error);
else $scope.notificationCount = results.length;
});
}
// update state of acknowledged notification
$scope.notificationAcknowledged = function () {
refreshNotifications();
};
function redirectOnMandatory2FA() {
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
$location.path('/profile').search({ setup2fa: true });
}
}
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
$scope.$on('$routeChangeStart', function (/* event */) {
if ($scope.initialized) redirectOnMandatory2FA();
});
var gPlatformStatusNotification = null;
function trackPlatformStatus() {
Client.getPlatformStatus(function (error, result) {
if (error) return console.error('Failed to get platform status.', error);
// see box/src/platform.js
if (result.message === 'Ready') {
if (gPlatformStatusNotification) {
gPlatformStatusNotification.kill();
gPlatformStatusNotification = null;
}
return;
}
if (!gPlatformStatusNotification) {
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
Notification.primary(options).then(function (result) {
gPlatformStatusNotification = result;
$timeout(trackPlatformStatus, 5000);
});
} else {
gPlatformStatusNotification.message = result.message;
$timeout(trackPlatformStatus, 5000);
}
});
}
function redirectIfNeeded(status) {
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
window.location.href = '/restore.html' + window.location.search;
} else if (status.adminFqdn) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html' + (window.location.search);
} else {
window.location.href = '/setupdns.html' + window.location.search;
}
return true;
}
// support local development with localhost check
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
// user is accessing by IP or by the old admin location (pre-migration)
window.location.href = '/setupdns.html' + window.location.search;
return true;
}
return false;
}
// this loads the very first thing when accessing via IP or domain
function init() {
Client.getProvisionStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (redirectIfNeeded(status)) return;
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
console.log('Running dashboard version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
Client.refreshConfig(function (error) {
if (error) return Client.initError(error, init);
Client.refreshAvailableLanguages(function (error) {
if (error) return Client.initError(error, init);
Client.refreshInstalledApps(function (error) {
if (error) return Client.initError(error, init);
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
if (Client.getUserInfo().hasBackgroundImage) {
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
document.getElementById('mainContentContainer').classList.add('has-background');
}
$scope.initialized = true;
redirectOnMandatory2FA();
$interval(refreshNotifications, 60 * 1000);
refreshNotifications();
Client.getSubscription(function (error, subscription) {
if (error && error.statusCode === 412) return; // not yet registered
if (error && error.statusCode === 402) return; // invalid appstore token
if (error) return console.error(error);
$scope.subscription = subscription;
// only track platform status if we are registered
trackPlatformStatus();
});
});
});
});
});
});
}
Client.onConfig(function (config) {
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
init();
// setup all the dialog focus handling
['updateModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
}]);
-202
View File
@@ -1,202 +0,0 @@
'use strict';
/* global angular */
/* global moment */
/* global $ */
// create main application module
var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
app.controller('LogsController', ['$scope', '$translate', 'Client', function ($scope, $translate, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.initialized = false;
$scope.client = Client;
$scope.selected = '';
$scope.activeEventSource = null;
$scope.lines = 100;
$scope.selectedAppInfo = null;
$scope.selectedTaskInfo = null;
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
$scope.clear = function () {
var logViewer = $('.logs-container');
logViewer.empty();
};
// https://github.com/janl/mustache.js/blob/master/mustache.js#L60
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
function showLogs() {
if (!$scope.selected) return;
var func;
if ($scope.selected.type === 'platform') func = Client.getPlatformLogs;
else if ($scope.selected.type === 'service') func = Client.getServiceLogs;
else if ($scope.selected.type === 'task') func = Client.getTaskLogs;
else if ($scope.selected.type === 'app') func = Client.getAppLogs;
func($scope.selected.value, true /* follow */, $scope.lines, function handleLogs(error, result) {
if (error) return console.error(error);
$scope.activeEventSource = result;
result.onmessage = function handleMessage(message) {
var data;
try {
data = JSON.parse(message.data);
} catch (e) {
return console.error(e);
}
// check if we want to auto scroll (this is before the appending, as that skews the check)
var tmp = $('.logs-container');
var autoScroll = tmp[0].scrollTop > (tmp[0].scrollHeight - tmp.innerHeight() - 24);
var logLine = $('<div class="log-line">');
// realtimeTimestamp is 0 if line is blank or some parse error
var timeString = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : '';
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message))));
tmp.append(logLine);
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
};
});
}
function select(ids, callback) {
if (ids.id && ids.id.indexOf('redis:') === 0) {
$scope.selected = {
name: 'Redis',
type: 'service',
value: ids.id,
url: Client.makeURL('/api/v1/services/' + ids.id + '/logs')
};
callback();
} else if (ids.id) {
var BUILT_IN_LOGS = [
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
{ name: 'Graphite', type: 'service', value: 'graphite', url: Client.makeURL('/api/v1/services/graphite/logs') },
{ name: 'MongoDB', type: 'service', value: 'mongodb', url: Client.makeURL('/api/v1/services/mongodb/logs') },
{ name: 'MySQL', type: 'service', value: 'mysql', url: Client.makeURL('/api/v1/services/mysql/logs') },
{ name: 'PostgreSQL', type: 'service', value: 'postgresql', url: Client.makeURL('/api/v1/services/postgresql/logs') },
{ name: 'Mail', type: 'service', value: 'mail', url: Client.makeURL('/api/v1/services/mail/logs') },
{ name: 'Docker', type: 'service', value: 'docker', url: Client.makeURL('/api/v1/services/docker/logs') },
{ name: 'Nginx', type: 'service', value: 'nginx', url: Client.makeURL('/api/v1/services/nginx/logs') },
{ name: 'Unbound', type: 'service', value: 'unbound', url: Client.makeURL('/api/v1/services/unbound/logs') },
{ name: 'SFTP', type: 'service', value: 'sftp', url: Client.makeURL('/api/v1/services/sftp/logs') },
{ name: 'TURN/STUN', type: 'service', value: 'turn', url: Client.makeURL('/api/v1/services/turn/logs') },
];
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
callback();
} else if (ids.crashId) {
$scope.selected = {
type: 'platform',
value: 'crash-' + ids.crashId,
name: 'Crash',
url: Client.makeURL('/api/v1/cloudron/logs/crash-' + ids.crashId)
};
callback();
} else if (ids.appId) {
Client.getApp(ids.appId, function (error, app) {
if (error) return callback(error);
$scope.selectedAppInfo = app;
$scope.selected = {
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
};
callback();
});
} else if (ids.taskId) {
Client.getTask(ids.taskId, function (error, task) {
if (error) return callback(error);
$scope.selectedTaskInfo = task;
$scope.selected = {
type: 'task',
value: task.id,
name: task.type,
url: Client.makeURL('/api/v1/tasks/' + task.id + '/logs')
};
callback();
});
}
}
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
console.log('Running log version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
Client.refreshConfig(function (error) {
if (error) return Client.initError(error, init);
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
if (error) return Client.initError(error, init);
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showLogs();
});
});
});
});
}
init();
$translate([ 'logs.title' ]).then(function (tr) {
if (tr['logs.title'] !== 'logs.title') window.document.title = tr['logs.title'];
});
}]);
-217
View File
@@ -1,217 +0,0 @@
'use strict';
/* global angular */
/* global $ */
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', '$interval', 'Notification', 'Client', function ($scope, $route, $timeout, $location, $interval, Notification, Client) {
$scope.initialized = false; // used to animate the UI
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.client = Client;
$scope.subscription = {};
$scope.notificationCount = 0;
$scope.hideNavBarActions = $location.path() === '/logs';
$scope.backgroundImageUrl = '';
$scope.reboot = {
busy: false,
show: function () {
$scope.reboot.busy = false;
$('#rebootModal').modal('show');
},
submit: function () {
$scope.reboot.busy = true;
Client.reboot(function (error) {
if (error) return Client.error(error);
$('#rebootModal').modal('hide');
// trigger refetch to show offline banner
$timeout(function () { Client.getStatus(function () {}); }, 5000);
});
}
};
$scope.isActive = function (url) {
if (!$route.current) return false;
return $route.current.$$route.originalPath.indexOf(url) === 0;
};
$scope.logout = function (event) {
event.stopPropagation();
$scope.initialized = false;
Client.logout();
};
$scope.openSubscriptionSetup = function () {
Client.openSubscriptionSetup($scope.subscription);
};
// NOTE: this function is exported and called from the appstore.js
$scope.updateSubscriptionStatus = function () {
Client.getSubscription(function (error, subscription) {
if (error && error.statusCode === 412) return; // not yet registered
if (error && error.statusCode === 402) return; // invalid appstore token
if (error) return console.error(error);
$scope.subscription = subscription;
});
};
function refreshNotifications() {
if (!Client.getUserInfo().isAtLeastAdmin) return;
Client.getNotifications({ acknowledged: false }, 1, 100, function (error, results) { // counter maxes out at 100
if (error) console.error(error);
else $scope.notificationCount = results.length;
});
}
// update state of acknowledged notification
$scope.notificationAcknowledged = function () {
refreshNotifications();
};
function redirectOnMandatory2FA() {
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
$location.path('/profile').search({ setup2fa: true });
}
}
// Make it redirect if the browser URL is changed directly - https://forum.cloudron.io/topic/7510/bug-in-2fa-force
$scope.$on('$routeChangeStart', function (/* event */) {
if ($scope.initialized) redirectOnMandatory2FA();
});
var gPlatformStatusNotification = null;
function trackPlatformStatus() {
Client.getPlatformStatus(function (error, result) {
if (error) return console.error('Failed to get platform status.', error);
// see box/src/platform.js
if (result.message === 'Ready') {
if (gPlatformStatusNotification) {
gPlatformStatusNotification.kill();
gPlatformStatusNotification = null;
}
return;
}
if (!gPlatformStatusNotification) {
var options = { title: 'Platform status', message: result.message, delay: 'notimeout', replaceMessage: true, closeOnClick: false };
Notification.primary(options).then(function (result) {
gPlatformStatusNotification = result;
$timeout(trackPlatformStatus, 5000);
});
} else {
gPlatformStatusNotification.message = result.message;
$timeout(trackPlatformStatus, 5000);
}
});
}
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
// WARNING if anything about the routing is changed here test these use-cases:
//
// 1. Caas
// 3. selfhosted restore
// 4. local development with gulp develop
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
if (status.restore.active || status.restore.errorMessage) { // show the error message in restore page
window.location.href = '/restore.html' + window.location.search;
} else {
window.location.href = (status.adminFqdn ? '/setup.html' : '/setupdns.html') + window.location.search;
}
return;
}
// support local development with localhost check
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost' && !window.location.hostname.startsWith('192.')) {
// user is accessing by IP or by the old admin location (pre-migration)
window.location.href = '/setupdns.html' + window.location.search;
return;
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
console.log('Running dashboard version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
Client.refreshConfig(function (error) {
if (error) return Client.initError(error, init);
Client.refreshAvailableLanguages(function (error) {
if (error) return Client.initError(error, init);
Client.refreshInstalledApps(function (error) {
if (error) return Client.initError(error, init);
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
if (Client.getUserInfo().hasBackgroundImage) {
document.getElementById('mainContentContainer').style.backgroundImage = 'url("' + Client.getBackgroundImageUrl() + '")';
document.getElementById('mainContentContainer').classList.add('has-background');
}
$scope.initialized = true;
redirectOnMandatory2FA();
$interval(refreshNotifications, 60 * 1000);
refreshNotifications();
Client.getSubscription(function (error, subscription) {
if (error && error.statusCode === 412) return; // not yet registered
if (error && error.statusCode === 402) return; // invalid appstore token
if (error) return console.error(error);
$scope.subscription = subscription;
// only track platform status if we are registered
trackPlatformStatus();
});
});
});
});
});
});
}
Client.onConfig(function (config) {
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
init();
// setup all the dialog focus handling
['updateModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
}]);
@@ -57,7 +57,7 @@ translateFilterFactory.displayName = 'translateFilterFactory';
app.filter('tr', translateFilterFactory);
app.controller('LoginController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
app.controller('PasswordResetController', ['$scope', '$translate', '$http', function ($scope, $translate, $http) {
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.indexOf('=') === -1 ? [item, true] : [item.slice(0, item.indexOf('=')), item.slice(item.indexOf('=')+1)]; }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
@@ -65,7 +65,7 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
$scope.mode = '';
$scope.busy = false;
$scope.error = false;
$scope.status = null;
$scope.branding = null;
$scope.username = '';
$scope.password = '';
$scope.totpToken = '';
@@ -74,50 +74,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
$scope.newPasswordRepeat = '';
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
$scope.onLogin = function () {
$scope.busy = true;
$scope.error = false;
var data = {
username: $scope.username,
password: $scope.password,
totpToken: $scope.totpToken
};
function error(data, status) {
$scope.busy = false;
$scope.error = {};
if (!data || status !== 401) return $scope.error.internal = true;
if (data.message === 'Username and password does not match') {
$scope.error.password = true;
$scope.password = '';
setTimeout(function () { $('#inputPassword').focus(); }, 200);
} else if (data.message.indexOf('totpToken') !== -1) {
$scope.error.totpToken = true;
$scope.totpToken = '';
setTimeout(function () { $('#inputTotpToken').focus(); }, 200);
} else {
$scope.error.generic = true;
}
$scope.loginForm.$setPristine();
}
$http.post(API_ORIGIN + '/api/v1/cloudron/login', data).success(function (data, status) {
if (status !== 200) return error(data, status);
localStorage.token = data.accessToken;
// prevent redirecting to random domains
var returnTo = search.returnTo || '/';
if (returnTo.indexOf('/') !== 0) returnTo = '/';
window.location.href = returnTo;
}).error(error);
};
$scope.onPasswordReset = function () {
$scope.busy = true;
@@ -130,7 +86,7 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
$scope.mode = 'passwordResetDone';
}
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset_request', data).success(done).error(done);
$http.post(API_ORIGIN + '/api/v1/auth/password_reset_request', data).success(done).error(done);
};
$scope.onNewPassword = function () {
@@ -151,7 +107,7 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
else $scope.error = 'Unknown error';
}
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset', data).success(function (data, status) {
$http.post(API_ORIGIN + '/api/v1/auth/password_reset', data).success(function (data, status) {
if (status !== 202) return error(data, status);
// set token to autologin
@@ -170,28 +126,20 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
};
$scope.showLogin = function () {
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
$scope.mode = 'login';
$scope.error = false;
setTimeout(function () { $('#inputUsername').focus(); }, 200);
};
$scope.showNewPassword = function () {
window.document.title = 'Set New Password';
$scope.mode = 'newPassword';
setTimeout(function () { $('#inputNewPassword').focus(); }, 200);
};
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
$scope.initialized = true;
if (status !== 200) return;
if (data.language) $translate.use(data.language);
if ($scope.mode === 'login') window.document.title = data.cloudronName + ' Login';
$scope.status = data;
$scope.branding = data;
}).error(function () {
$scope.initialized = false;
});
@@ -205,6 +153,6 @@ app.controller('LoginController', ['$scope', '$translate', '$http', function ($s
localStorage.token = search.accessToken || search.access_token;
window.location.href = '/';
} else {
$scope.showLogin();
$scope.showPasswordReset();
}
}]);
+30 -8
View File
@@ -41,6 +41,8 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
$scope.encrypted = false; // only used if a backup config contains that flag
$scope.setupToken = '';
$scope.skipDnsSetup = false;
$scope.disk = null;
$scope.blockDevices = [];
$scope.mountOptions = {
host: '',
@@ -54,6 +56,11 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
privateKey: ''
};
$scope.$watch('disk', function (newValue) {
if (!newValue) return;
$scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
});
$scope.sysinfo = {
provider: 'generic',
ipv4: '',
@@ -98,7 +105,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
};
$scope.mountlike = function (provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
};
$scope.restore = function () {
@@ -195,7 +202,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
backupConfig.mountOptions.port = $scope.mountOptions.port;
backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey;
}
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
} else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath;
} else if (backupConfig.provider === 'mountpoint') {
backupConfig.mountPoint = $scope.mountPoint;
@@ -284,7 +291,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
function waitForRestore() {
$scope.busy = true;
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (!error && !status.restore.active) { // restore finished
if (status.restore.errorMessage) {
$scope.busy = false;
@@ -346,7 +353,7 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
};
function init() {
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (status.restore.active) return waitForRestore();
@@ -358,10 +365,25 @@ app.controller('RestoreController', ['$scope', 'Client', function ($scope, Clien
return;
}
$scope.status = status;
$scope.instanceId = search.instanceId;
$scope.setupToken = search.setupToken;
$scope.initialized = true;
Client.getProvisionBlockDevices(function (error, result) {
if (error) {
console.error('Failed to list blockdevices:', error);
} else {
// only offer non /, /boot or /home disks
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
// only offer xfs and ext4 disks
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
// amend label for UI
result.forEach(function (d) { d.label = d.path; });
}
$scope.blockDevices = result;
$scope.instanceId = search.instanceId;
$scope.setupToken = search.setupToken;
$scope.initialized = true;
});
});
}
+8 -6
View File
@@ -71,22 +71,24 @@ app.controller('SetupController', ['$scope', 'Client', function ($scope, Client)
return;
}
// if we are here from the ip first go to the real domain if already setup
// if we are here from https://ip/setup.html ,go to https://admin/setup.html
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
return;
return true;
}
// if we don't have a domain yet, first go to domain setup
if (!status.adminFqdn) {
window.location.href = '/setupdns.html';
return;
return true;
}
if (status.activated) {
window.location.href = '/';
return;
return true;
}
return false;
}
function setView(view) {
@@ -98,10 +100,10 @@ app.controller('SetupController', ['$scope', 'Client', function ($scope, Client)
}
function init() {
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (error) return Client.initError(error, init);
redirectIfNeeded(status);
if (redirectIfNeeded(status)) return;
setView(search.view);
$scope.setupToken = search.setupToken;
+4 -4
View File
@@ -70,7 +70,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
$scope.busy = false;
$scope.error = null;
$scope.view = 'setup';
$scope.status = null;
$scope.branding = null;
$scope.profileLocked = !!search.profileLocked;
$scope.existingUsername = !!search.username;
@@ -119,7 +119,7 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
}
}
$http.post(API_ORIGIN + '/api/v1/cloudron/setup_account', data).success(function (data, status) {
$http.post(API_ORIGIN + '/api/v1/auth/setup_account', data).success(function (data, status) {
if (status !== 201) return error(data, status);
// set token to autologin
@@ -133,14 +133,14 @@ app.controller('SetupAccountController', ['$scope', '$translate', '$http', funct
$scope.view = 'noUsername';
$scope.initialized = true;
} else {
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
$http.get(API_ORIGIN + '/api/v1/auth/branding').success(function (data, status) {
$scope.initialized = true;
if (status !== 200) return;
if (data.language) $translate.use(data.language);
$scope.status = data;
$scope.branding = data;
}).error(function () {
$scope.initialized = false;
});
+3 -3
View File
@@ -249,7 +249,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
config: config,
tlsConfig: tlsConfig
},
sysinfoConfig: sysinfoConfig,
ipv4Config: sysinfoConfig,
providerToken: $scope.instanceId,
setupToken: $scope.setupToken
};
@@ -276,7 +276,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
function waitForDnsSetup() {
$scope.state = 'waitingForDnsSetup';
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (!error && !status.setup.active) {
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
$scope.error.setup = status.setup.errorMessage;
@@ -295,7 +295,7 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
}
function initialize() {
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (error) {
// During domain migration, the box code restarts and can result in getStatus() failing temporarily
console.error(error);
-376
View File
@@ -1,376 +0,0 @@
'use strict';
/* global angular, $, Terminal, AttachAddon, FitAddon, ISTATES */
// create main application module
angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification']);
angular.module('Application').controller('TerminalController', ['$scope', '$translate', '$timeout', '$location', 'Client', function ($scope, $translate, $timeout, $location, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.apps = [];
$scope.selected = '';
$scope.terminal = null;
$scope.terminalSocket = null;
$scope.fitAddon = null;
$scope.restartAppBusy = false;
$scope.appBusy = false;
$scope.selectedAppInfo = null;
$scope.schedulerTasks = [];
$scope.downloadFile = {
error: '',
filePath: '',
busy: false,
downloadUrl: function () {
if (!$scope.downloadFile.filePath) return '';
var filePath = encodeURIComponent($scope.downloadFile.filePath);
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
},
show: function () {
$scope.downloadFile.busy = false;
$scope.downloadFile.error = '';
$scope.downloadFile.filePath = '';
$('#downloadFileModal').modal('show');
},
submit: function () {
$scope.downloadFile.busy = true;
Client.checkDownloadableFile($scope.selected.value, $scope.downloadFile.filePath, function (error) {
$scope.downloadFile.busy = false;
if (error) {
$scope.downloadFile.error = 'The requested file does not exist.';
return;
}
// we have to click the link to make the browser do the download
// don't know how to prevent the browsers
$('#fileDownloadLink')[0].click();
$('#downloadFileModal').modal('hide');
});
}
};
$scope.uploadProgress = {
busy: false,
total: 0,
current: 0,
show: function () {
$scope.uploadProgress.total = 0;
$scope.uploadProgress.current = 0;
$('#uploadProgressModal').modal('show');
},
hide: function () {
$('#uploadProgressModal').modal('hide');
}
};
$scope.uploadFile = function () {
var fileUpload = document.querySelector('#fileUpload');
fileUpload.onchange = function (e) {
if (e.target.files.length === 0) return;
$scope.uploadProgress.busy = true;
$scope.uploadProgress.show();
Client.uploadFile($scope.selected.value, e.target.files[0], function progress(e) {
$scope.uploadProgress.total = e.total;
$scope.uploadProgress.current = e.loaded;
}, function (error) {
if (error) console.error(error);
$scope.uploadProgress.busy = false;
$scope.uploadProgress.hide();
});
};
fileUpload.click();
};
$scope.usesAddon = function (addon) {
if (!$scope.selected || !$scope.selected.addons) return false;
return !!Object.keys($scope.selected.addons).find(function (a) { return a === addon; });
};
function reset() {
if ($scope.terminal) {
$scope.terminal.dispose();
$scope.terminal = null;
}
if ($scope.terminalSocket) {
$scope.terminalSocket = null;
}
$scope.selectedAppInfo = null;
}
$scope.restartApp = function () {
$scope.restartAppBusy = true;
$scope.appBusy = true;
var appId = $scope.selected.value;
function waitUntilRestarted(callback) {
refreshApp(appId, function (error, result) {
if (error) return callback(error);
if (result.installationState === ISTATES.INSTALLED) return callback();
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
});
}
Client.restartApp(appId, function (error) {
if (error) console.error('Failed to restart app.', error);
waitUntilRestarted(function (error) {
if (error) console.error('Failed wait for restart.', error);
$scope.restartAppBusy = false;
$scope.appBusy = false;
});
});
};
function createTerminalSocket(callback) {
var appId = $scope.selected.value;
Client.createExec(appId, { cmd: [ '/bin/bash' ], tty: true, lang: 'C.UTF-8' }, function (error, execId) {
if (error) return callback(error);
try {
// websocket cannot use relative urls
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + appId + '/exec/' + execId + '/startws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
$scope.terminalSocket = new WebSocket(url);
$scope.terminal.loadAddon(new AttachAddon.AttachAddon($scope.terminalSocket));
$scope.terminalSocket.onclose = function () {
// retry in one second
$scope.terminalReconnectTimeout = setTimeout(function () {
showTerminal(true);
}, 1000);
};
callback();
} catch (e) {
callback(e);
}
});
}
function refreshApp(id, callback) {
Client.getApp(id, function (error, result) {
if (error) return callback(error);
$scope.selectedAppInfo = result;
callback(null, result);
});
}
function showTerminal(retry) {
reset();
if (!$scope.selected) return;
var appId = $scope.selected.value;
refreshApp(appId, function (error) {
if (error) return console.error(error);
var result = $scope.selectedAppInfo;
$scope.schedulerTasks = result.manifest.addons.scheduler ? Object.keys(result.manifest.addons.scheduler).map(function (k) { return { name: k, command: result.manifest.addons.scheduler[k].command }; }) : [];
$scope.terminal = new Terminal();
$scope.fitAddon = new FitAddon.FitAddon();
$scope.terminal.loadAddon($scope.fitAddon);
$scope.terminal.open(document.querySelector('#terminalContainer'));
window.terminal = $scope.terminal;
// Let the browser handle paste
$scope.terminal.attachCustomKeyEventHandler(function (e) {
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) return false;
});
if (retry) $scope.terminal.writeln('Reconnecting...');
else $scope.terminal.writeln('Connecting...');
// we have to give it some time to setup the terminal to make it fit, there is no event unfortunately
setTimeout(function () {
if (!$scope.terminal) return;
// this is here so that the text wraps correctly after the fit!
// var YELLOW = '\u001b[33m'; // https://gist.github.com/dainkaplan/4651352
// var NC = '\u001b[0m';
// $scope.terminal.writeln(YELLOW + 'If you resize the browser window, press Ctrl+D to start a new session with the current size.' + NC);
// we have to first write something on reconnect after app restart..not sure why
$scope.fitAddon.fit();
// create exec container after we fit() since we cannot resize exec container post-creation
createTerminalSocket(function (error) { if (error) console.error(error); });
$scope.terminal.focus();
}, 1000);
});
}
$scope.terminalInject = function (addon, extra) {
if (!$scope.terminalSocket) return;
var cmd, manifestVersion = $scope.selected.manifest.manifestVersion;
if (addon === 'mysql') {
if (manifestVersion === 1) {
cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
} else {
cmd = 'mysql --user=${CLOUDRON_MYSQL_USERNAME} --password=${CLOUDRON_MYSQL_PASSWORD} --host=${CLOUDRON_MYSQL_HOST} ${CLOUDRON_MYSQL_DATABASE}';
}
} else if (addon === 'postgresql') {
if (manifestVersion === 1) {
cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
} else {
cmd = 'PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE}';
}
} else if (addon === 'mongodb') {
if (manifestVersion === 1) {
cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
} else {
cmd = 'mongosh -u "${CLOUDRON_MONGODB_USERNAME}" -p "${CLOUDRON_MONGODB_PASSWORD}" ${CLOUDRON_MONGODB_HOST}:${CLOUDRON_MONGODB_PORT}/${CLOUDRON_MONGODB_DATABASE}';
}
} else if (addon === 'redis') {
if (manifestVersion === 1) {
cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
} else {
cmd = 'redis-cli -h "${CLOUDRON_REDIS_HOST}" -p "${CLOUDRON_REDIS_PORT}" -a "${CLOUDRON_REDIS_PASSWORD}"';
}
} else if (addon === 'scheduler' && extra) {
cmd = extra.command;
}
if (!cmd) return;
cmd += ' ';
$scope.terminalSocket.send(cmd);
$scope.terminal.focus();
};
// terminal right click handling
$scope.terminalClear = function () {
if (!$scope.terminal) return;
$scope.terminal.clear();
$scope.terminal.focus();
};
$scope.terminalCopy = function () {
if (!$scope.terminal) return;
// execCommand('copy') would copy any selection from the page, so do this only if terminal has a selection
if (!$scope.terminal.getSelection()) return;
document.execCommand('copy');
$scope.terminal.focus();
};
$('.contextMenuBackdrop').on('click', function () {
$('#terminalContextMenu').hide();
$('.contextMenuBackdrop').hide();
$scope.terminal.focus();
});
$('#terminalContainer').on('contextmenu', function (e) {
if (!$scope.terminal) return true;
e.preventDefault();
$('.contextMenuBackdrop').show();
$('#terminalContextMenu').css({
display: 'block',
left: e.pageX,
top: e.pageY
});
return false;
});
window.addEventListener('resize', function () {
if ($scope.fitAddon) $scope.fitAddon.fit();
});
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
if (!status.activated) {
console.log('Not activated yet, closing or redirecting', status);
window.close();
window.location.href = '/';
return;
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
console.log('Running terminal version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return $scope.error(error);
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
refreshApp(search.id, function (error, app) {
$scope.selected = {
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons,
manifest: app.manifest
};
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showTerminal();
});
});
});
});
$translate([ 'terminal.title' ]).then(function (tr) {
if (tr['terminal.title'] !== 'terminal.title') window.document.title = tr['terminal.title'];
});
// setup all the dialog focus handling
['downloadFileModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
}]);
-84
View File
@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html ng-app="Application" ng-controller="LogsController">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title>Cloudron Logs</title>
<meta name="description" content="Cloudron Logs">
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
<link rel="icon" href="/api/v1/cloudron/avatar">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Fontawesome -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
<!-- async -->
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
<!-- Angular translate https://angular-translate.github.io/ -->
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
<!-- Showdown (markdown converter) -->
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
<!-- colors -->
<script type="text/javascript" src="/3rdparty/js/colors.js?<%= revision %>"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment-with-locales.min.js?<%= revision %>"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/logs.js?<%= revision %>"></script>
</head>
<body class="logs">
<a class="offline-banner animateMe" ng-show="client.offline" ng-cloak href="https://docs.cloudron.io/troubleshooting/" target="_blank"><i class="fa fa-circle-notch fa-spin"></i> {{ 'main.offline' | tr }}</a>
<div class="animateMe ng-hide layout-root" ng-show="initialized">
<div class="logs-controls">
<h3 style="display: inline-block;">{{ selected.name }}</h3>
<!-- logs actions -->
<div class="pull-right">
<a class="btn btn-primary" ng-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> {{ 'terminal.title' | tr }}</a>
<a class="btn btn-primary" ng-href="{{ '/filemanager.html?type=app&id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> {{ 'filemanager.title' | tr }}</a>
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> {{ 'logs.clear' | tr }}</a>
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=-1"><i class="fa fa-download"></i> {{ 'logs.download' | tr }}</a>
</div>
</div>
<div class="logs-container"></div>
</div>
</body>
</html>
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title>Cloudron Not Found</title>
<meta name="description" content="Cloudron Not Found">
<title>Cloudron - Not Found</title>
<meta name="description" content="Cloudron - Not Found">
<!-- Use static style as we can't include local stylesheets -->
<style>
@@ -6,8 +6,8 @@
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
<title>Cloudron Login</title>
<meta name="description" content="Cloudron Login">
<title>Cloudron Password Reset</title>
<meta name="description" content="Cloudron Password Reset">
<link id="favicon" href="<%= apiOrigin %>/api/v1/cloudron/avatar" rel="icon" type="image/png">
@@ -47,54 +47,14 @@
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
<!-- Setup Application -->
<script type="text/javascript" src="/js/login.js?<%= revision %>"></script>
<script type="text/javascript" src="/js/passwordreset.js?<%= revision %>"></script>
</head>
<body ng-app="Application" ng-controller="LoginController">
<body ng-app="Application" ng-controller="PasswordResetController">
<div class="layout-root ng-cloak" ng-show="initialized">
<div class="layout-content" ng-show="mode === 'login'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h1><small>{{ 'login.loginTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4 class="has-error" ng-show="error && (error.generic || error.password)">{{ 'login.errorIncorrectCredentials' | tr }}</h4>
<h4 class="has-error" ng-show="error && error.totpToken">{{ 'login.errorIncorrect2FAToken' | tr }}</h4>
<h4 class="has-error" ng-show="error && error.internal">{{ 'login.errorInternal' | tr }}</h4>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form name="loginForm" ng-submit="onLogin()">
<div class="form-group">
<label class="control-label" for="inputUsername">{{ 'login.username' | tr }}</label>
<input type="text" class="form-control" id="inputUsername" name="username" ng-model="username" ng-disabled="busy" autofocus required>
</div>
<div class="form-group" ng-class="{'has-error': error.password }">
<label class="control-label" for="inputPassword">{{ 'login.password' | tr }}</label>
<input type="password" class="form-control" name="password" id="inputPassword" ng-model="password" ng-disabled="busy" required password-reveal>
</div>
<div class="form-group" ng-class="{'has-error': error.totpToken }">
<label class="control-label" for="inputTotpToken">{{ 'login.2faToken' | tr }}</label>
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
</div>
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || loginForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'login.signInAction' | tr }}</button>
</form>
<a ng-href="" class="hand" ng-click="showPasswordReset()">{{ 'login.resetPasswordAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'passwordReset'">
<div class="card" style="padding: 20px; margin-top: 100px; max-width: 620px;">
<div class="row">
@@ -113,9 +73,11 @@
<input type="text" class="form-control" id="inputPasswordResetIdentifier" name="passwordResetIdentifier" ng-model="passwordResetIdentifier" ng-disabled="busy" autofocus required>
</div>
<br/>
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
<div class="card-form-bottom-bar">
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || passwordResetForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.resetAction' | tr }}</button>
</div>
</form>
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
@@ -129,7 +91,7 @@
<br/>
<h2>{{ 'passwordReset.emailSent.title' | tr }}</h2>
<br/>
<button class="btn btn-primary" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</button>
<a href="/" class="btn btn-primary">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
@@ -173,10 +135,11 @@
<label class="control-label" for="inputPasswordResetTotpToken">{{ 'login.2faToken' | tr }}</label>
<input type="text" class="form-control" name="passwordResetTotpToken" id="inputPasswordResetTotpToken" ng-model="totpToken" ng-disabled="busy" value="">
</div>
<br/>
<button class="btn btn-primary btn-outline pull-right" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
<div class="card-form-bottom-bar">
<a href="/" class="hand">{{ 'passwordReset.backToLoginAction' | tr }}</a>
<button class="btn btn-primary btn-outline" type="submit" ng-disabled="busy || newPasswordForm.$invalid || newPassword !== newPasswordRepeat"><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> {{ 'passwordReset.passwordChanged.submitAction' | tr }}</button>
</div>
</form>
<a ng-href="" class="hand" ng-click="showLogin()">{{ 'passwordReset.backToLoginAction' | tr }}</a>
</div>
</div>
</div>
@@ -197,7 +160,7 @@
</div>
<footer class="text-center">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
</footer>
</div>
+6
View File
@@ -137,6 +137,12 @@
<input type="text" class="form-control" ng-model="mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="busy" placeholder="Directory for backups" ng-required="provider === 'ext4' || provider === 'xfs'">
</div>
<!-- Disk -->
<div class="form-group" ng-class="{ 'has-error': error.diskPath }" ng-show="provider === 'disk'">
<label class="control-label">Device</label>
<select class="form-control" ng-model="disk" ng-options="item as item.label for item in blockDevices track by item.path" ng-required="provider === 'disk'"></select>
</div>
<!-- SSHFS -->
<div class="form-group" ng-show="provider === 'sshfs'">
<label class="control-label" for="configureBackupPort">SSH Port</label>
+2 -2
View File
@@ -58,7 +58,7 @@
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" style="margin-top: -84px" src="<%= apiOrigin %>/api/v1/cloudron/avatar"/>
<br/>
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ status.cloudronName || 'Cloudron' }}</h1>
<h1><small>{{ 'setupAccount.welcomeTo' | tr }}</small> {{ branding.cloudronName || 'Cloudron' }}</h1>
<h3>{{ 'setupAccount.description' | tr }}</h3>
</div>
</div>
@@ -154,7 +154,7 @@
</div>
<footer class="text-center ng-cloak">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
<span class="text-muted" ng-bind-html="branding.footer | markdown2html"></span>
</footer>
</div>
-172
View File
@@ -1,172 +0,0 @@
<!DOCTYPE html>
<html ng-app="Application" ng-controller="TerminalController">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<title>Cloudron Terminal</title>
<meta name="description" content="Cloudron Terminal">
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
<link rel="icon" href="/api/v1/cloudron/avatar">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Fontawesome -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js?<%= revision %>"></script>
<!-- async -->
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js?<%= revision %>"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.min.js?<%= revision %>"></script>
<!-- Angularjs scripts -->
<script type="text/javascript" src="/3rdparty/js/angular.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-loader.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-cookies.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-animate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-base64.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-md5.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-sanitize.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js?<%= revision %>"></script>
<!-- Angular translate https://angular-translate.github.io/ -->
<script type="text/javascript" src="/3rdparty/js/angular-translate.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-loader-static-files.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-cookie.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-translate-storage-local.min.js?<%= revision %>"></script>
<!-- Clipboard handling -->
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js?<%= revision %>"></script>
<!-- xterm -->
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/css/xterm.css?<%= revision %>" />
<script src="/3rdparty/xterm/lib/xterm.js?<%= revision %>"></script>
<script src="/3rdparty/xterm-addon-attach/lib/xterm-addon-attach.js?<%= revision %>"></script>
<script src="/3rdparty/xterm-addon-fit/lib/xterm-addon-fit.js?<%= revision %>"></script>
<!-- Showdown (markdown converter) -->
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.min.js?<%= revision %>"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/terminal.js?<%= revision %>"></script>
</head>
<body style="overflow: hidden;">
<!-- Modal download file -->
<div class="modal fade" id="downloadFileModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'terminal.download.title' | tr:{ name: selected.name } }}</h4>
</div>
<div class="modal-body">
<form name="downloadFileForm" ng-submit="downloadFile.submit()">
<div class="form-group" ng-class="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
<label class="control-label" for="inputDownloadFilePath">{{ 'terminal.download.filePath' | tr }}</label>
<div class="control-label" ng-show="{ 'has-error': downloadFileForm.filePath.$dirty && downloadFile.error }">
<small>{{ downloadFile.error }}</small>
</div>
<input type="text" class="form-control" name="filePath" ng-model="downloadFile.filePath" required autofocus>
</div>
<input id="inputDownloadFilePath" class="ng-hide" type="submit" ng-disabled="!downloadFile.filePath"/>
</form>
</div>
<div class="modal-footer">
<a id="fileDownloadLink" class="" ng-href="{{ downloadFile.downloadUrl() }}" target="_blank"></a>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="downloadFile.submit()" ng-disabled="!downloadFile.filePath"><i class="fa fa-circle-notch fa-spin" ng-show="downloadFile.busy"></i> {{ 'terminal.download.download' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal upload progress -->
<div class="modal fade" id="uploadProgressModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'terminal.upload.title' | tr:{ name: selected.name } }}</h4>
</div>
<div class="modal-body">
<span><b>{{ uploadProgress.current | prettyDecimalSize }}</b> (total {{ uploadProgress.total | prettyDecimalSize }})</span>
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ 100*(uploadProgress.current/uploadProgress.total) }}%"></div>
</div>
</div>
<div class="modal-footer"></div>
</div>
</div>
</div>
<div class="animateMe ng-hide layout-root terminal-view" ng-show="initialized">
<div class="terminal-controls">
<h3 style="display: inline-block;">{{ selected.name }}</h3>
<input type="file" id="fileUpload" class="hide"/>
<div class="pull-right">
<div class="btn-group" ng-show="usesAddon('scheduler')">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-disabled="appBusy">
{{ 'terminal.scheduler' | tr }} <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in schedulerTasks"><a href="" ng-click="terminalInject('scheduler', task)">{{ task.name }}</a></li>
</ul>
</div>
<!-- addon actions -->
<button class="btn btn-success" ng-click="terminalInject('mysql')" ng-show="usesAddon('mysql')" ng-disabled="appBusy">MySQL</button>
<button class="btn btn-success" ng-click="terminalInject('postgresql')" ng-show="usesAddon('postgresql')" ng-disabled="appBusy">Postgres</button>
<button class="btn btn-success" ng-click="terminalInject('mongodb')" ng-show="usesAddon('mongodb')" ng-disabled="appBusy">MongoDB</button>
<button class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')" ng-disabled="appBusy">Redis</button>
<!-- terminal actions -->
<button class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': restartAppBusy }"></i> {{ 'terminal.restart' | tr }}</button>
<button class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-upload"></i> {{ 'terminal.uploadToTmp' | tr }}</button>
<button class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy" ng-disabled="appBusy"><i class="fa fa-circle-notch fa-spin"></i> {{ 'terminal.uploading' | tr }}</button>
<button class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'" ng-disabled="appBusy"><i class="fa fa-download"></i> {{ 'terminal.downloadAction' | tr }}</button>
</div>
</div>
<div class="terminal-container" id="terminalContainer" ng-hide="appBusy"></div>
<div class="terminal-container placeholder" ng-show="appBusy">
<h4>&nbsp;
<span ng-show="restartAppBusy">{{ 'terminal.busy.restarting' | tr }}</span>
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && selectedAppInfo.debugMode">{{ 'terminal.busy.restartingInPausedMode' | tr }}</span>
<span ng-show="selectedAppInfo.installationState === 'pending_debug' && !selectedAppInfo.debugMode ">{{ 'terminal.busy.resuming' | tr }}</span>
<span ng-show="selectedAppInfo.installationState === 'pending_installed'">{{ 'terminal.busy.installing' | tr }}</span>
</h4>
<div class="progress" ng-show="appBusy" style="width: 80%">
<div class="progress-bar progress-bar-striped active" role="progressbar" style="width: 100%"></div>
</div>
</div>
<div class="contextMenuBackdrop">
<ul class="dropdown-menu" id="terminalContextMenu" style="position: absolute; display:none;">
<li><a href="" ng-click="terminalCopy()">{{ 'terminal.contextmenu.copy' | tr }}</a></li>
<li class="disabled"><a>{{ 'terminal.contextmenu.pasteInfo' | tr }}</a></li>
<li role="separator" class="divider"></li>
<li><a href="" ng-click="terminalClear()">{{ 'terminal.contextmenu.clear' | tr }}</a></li>
</ul>
</div>
</div>
</body>
</html>
+32 -2
View File
@@ -320,6 +320,10 @@ h1, h2, h3 {
z-index: 200;
}
.section-header {
margin-top: 50px;
}
.offscreen {
position: absolute;
left: -999em;
@@ -685,7 +689,7 @@ multiselect {
}
.card {
min-height: 488px;
min-height: 523px;
}
@media(min-width:768px) {
@@ -824,6 +828,19 @@ multiselect {
cursor: not-allowed;
}
// ----------------------------
// Login and password forms
// ----------------------------
.card-form-bottom-bar {
display: flex;
justify-content: space-between;
}
.card-form-bottom-bar > * {
align-self: center;
}
// ----------------------------
// Appstore view
// ----------------------------
@@ -1757,6 +1774,14 @@ tag-input {
.logs {
background: black;
.logs-error {
color: white;
width: 100%;
font-size: 18px;
text-align: center;
margin-top: 200px;
}
.logs-controls {
margin: 5px;
@@ -2023,8 +2048,13 @@ tag-input {
.has-background {
h1, h2, h3 {
filter: drop-shadow(0 0 0.5px black);
color: white;
-webkit-text-stroke: 0.3px black;
.btn {
color: white;
-webkit-text-stroke: 0;
}
}
.modal-content {
+50 -6
View File
@@ -51,7 +51,8 @@
"save": "Gem",
"close": "Luk",
"no": "Nej",
"yes": "Ja"
"yes": "Ja",
"delete": "Slet"
},
"username": "Brugernavn",
"displayName": "Vis navn",
@@ -87,7 +88,8 @@
"statusEnabled": "Aktiveret",
"statusDisabled": "Slået fra",
"loadingPlaceholder": "Indlæsning",
"disableAction": "Deaktiver"
"disableAction": "Deaktiver",
"settings": "Indstillinger"
},
"appstore": {
"category": {
@@ -1013,7 +1015,8 @@
"hetznerToken": "Hetzner Token",
"porkbunSecretapikey": "Hemmelig API-nøgle",
"cloudflareDefaultProxyStatus": "Aktiver proxying for nye DNS-poster",
"porkbunApikey": "API-nøgle"
"porkbunApikey": "API-nøgle",
"bunnyAccessKey": "Bunny Access Key"
},
"title": "Domæner og certs",
"addDomain": "Tilføj domæne",
@@ -1042,7 +1045,8 @@
"domainWellKnown": {
"title": "Well-Known locations på {{ domain }}"
},
"tooltipWellKnown": "Indstil well-known lokationer"
"tooltipWellKnown": "Indstil well-known lokationer",
"count": "Samlede domæner: {{ count }}"
},
"notifications": {
"markAllAsRead": "Markér alle som læst",
@@ -1817,7 +1821,9 @@
"password": "Adgangskode",
"2faToken": "2FA-token (hvis aktiveret)",
"signInAction": "Log ind",
"resetPasswordAction": "Nulstil adgangskode"
"resetPasswordAction": "Nulstil adgangskode",
"errorIncorrect2FAToken": "2FA-token er ugyldig",
"errorInternal": "Intern fejl, prøv igen senere"
},
"lang": {
"en": "English",
@@ -1831,9 +1837,47 @@
"zh_Hans": "Kinesisk (forenklet)",
"es": "Spansk",
"ru": "Russisk",
"pt": "Portugisisk"
"pt": "Portugisisk",
"da": "Dansk"
},
"supportConfig": {
"emailNotVerified": "Du bedes først bekræfte e-mailen på cloudron.io-kontoen for at sikre, at vi kan kontakte dig."
},
"oidc": {
"newClientDialog": {
"title": "Tilføj klient",
"description": "Tilføj nye OpenID connect-klientindstillinger.",
"createAction": "Opret"
},
"client": {
"name": "Navn",
"id": "Klient-id",
"secret": "Klientens secret",
"signingAlgorithm": "Signeringsalgoritme",
"loginRedirectUri": "Url til tilbagekaldelse af login (kommasepareret, hvis der er mere end én)",
"logoutRedirectUri": "Url til tilbagekaldelse af logout (valgfrit)"
},
"title": "OpenID Connect-udbyder",
"description": "Cloudron kan fungere som OpenID Connect-udbyder for interne apps og eksterne tjenester.",
"editClientDialog": {
"title": "Rediger klient {{ client }}"
},
"deleteClientDialog": {
"title": "Virkelig slette klient {{ client }}?",
"description": "Dette vil afbryde forbindelsen til alle eksterne OpenID-apps fra denne Cloudron, der bruger dette klient-id."
},
"env": {
"discoveryUrl": "URL til opdagelse",
"logoutUrl": "URL til logout",
"profileEndpoint": "Profil slutpunkt",
"keysEndpoint": "Nøgler Slutpunkt",
"tokenEndpoint": "Token slutpunkt",
"authEndpoint": "Auth-slutpunkt"
},
"clients": {
"title": "Klienter",
"newClient": "Ny klient",
"empty": "Ingen klienten endnu"
}
}
}
+3 -2
View File
@@ -242,7 +242,7 @@
"subscriptionRequired": "Diese Funktionen sind nur im Abo enthalten.",
"require2FACheckbox": "User müssen Zwei-Faktor-Authentifizierung (2FA) aktivieren",
"allowProfileEditCheckbox": "Erlaube Usern ihren Namen und E-Mail-Adresse zu ändern",
"title": "Einstellungen",
"title": "User Einstellungen",
"require2FAWarning": "Richte 2FA ein um nicht ausgesperrt zu werden."
},
"groups": {
@@ -1366,7 +1366,8 @@
"paste": "Einfügen",
"copy": "Kopieren",
"cut": "Ausschneiden",
"edit": "Bearbeiten"
"edit": "Bearbeiten",
"open": "Öffnen"
},
"symlink": "Symlink zu {{ target }}",
"mtime": "Geändert"
+50 -13
View File
@@ -56,7 +56,8 @@
},
"action": {
"reboot": "Reboot",
"logs": "Logs"
"logs": "Logs",
"showLogs": "Show Logs"
},
"clipboard": {
"copied": "Copied to clipboard",
@@ -89,7 +90,8 @@
"statusEnabled": "Enabled",
"statusDisabled": "Disabled",
"loadingPlaceholder": "Loading",
"settings": "Settings"
"settings": "Settings",
"saveAction": "Save"
},
"appstore": {
"title": "App Store",
@@ -206,7 +208,7 @@
"externalLdapTooltip": "From external LDAP directory"
},
"settings": {
"title": "Settings",
"title": "User Settings",
"allowProfileEditCheckbox": "Allow users to edit their name and email",
"require2FACheckbox": "Require users to set up 2FA",
"subscriptionRequired": "These features are only available in the paid plans.",
@@ -662,7 +664,7 @@
"settings": {
"title": "Settings",
"info": "These settings are global and apply to all domains.",
"location": "Mail server location",
"location": "Mail Server Location",
"maxMailSize": "Maximum email size",
"spamFilter": "Spam filtering",
"spamFilterOverview": "{{ blacklistCount }} address(es) on the blocklist.",
@@ -707,7 +709,7 @@
},
"changeDomainDialog": {
"title": "Change Email Server Location",
"description": "Cloudron will make the necessary DNS changes across all the domains and restart the mail server. Desktop & Mobile email clients have to be re-configured to use this new location as the IMAP and SMTP server.",
"description": "This will move the IMAP and SMTP server to the specified location.",
"location": "Location",
"locationPlaceholder": "Leave empty to use bare domain",
"manualInfo": "Add an A record manually for {{ domain }} to this Cloudron's public IP"
@@ -790,7 +792,8 @@
},
"dyndns": {
"title": "Dynamic DNS",
"description": "Enable this option to keep all your DNS records in sync with a changing IP address. This is useful when Cloudron runs in a network with a frequently changing public IP address like a home connection."
"description": "Enable this option to keep all your DNS records in sync with a changing IP address. This is useful when Cloudron runs in a network with a frequently changing public IP address like a home connection.",
"showLogsAction": "Show Logs"
},
"configureIp": {
"title": "Configure IP Provider",
@@ -806,7 +809,13 @@
},
"configureIpv6": {
"title": "Configure IPv6 Provider"
}
},
"trustedIps": {
"description": "HTTP headers from matching IP addresses will be trusted",
"title": "Configure Trusted IPs",
"summary": "{{ trustCount }} IPs trusted"
},
"trustedIpRanges": "Trusted IPs & Ranges "
},
"services": {
"title": "Services",
@@ -980,7 +989,7 @@
},
"changeDashboardDomain": {
"title": "Change Dashboard Domain",
"description": "This will move the dashboard and the email server to the <code>my</code>subdomain of the selected domain.",
"description": "This will move the dashboard to the <code>my</code>subdomain of the selected domain.",
"changeAction": "Change Domain",
"cancelAction": "Cancel",
"showLogsAction": "Show Logs"
@@ -1066,7 +1075,9 @@
"logs": {
"title": "Logs",
"clear": "Clear View",
"download": "Download Full Logs"
"download": "Download Full Logs",
"notFoundError": "No such task or app",
"logsGoneError": "Log file(s) not found"
},
"terminal": {
"title": "Terminal",
@@ -1164,7 +1175,8 @@
"cut": "Cut",
"copy": "Copy",
"paste": "Paste",
"selectAll": "Select All"
"selectAll": "Select All",
"open": "Open"
},
"mtime": "Modified"
},
@@ -1179,7 +1191,19 @@
},
"status": {
"restartingApp": "restarting app"
}
},
"uploader": {
"uploading": "Uploading",
"exitWarning": "Upload still in progress. Really close this page?"
},
"textEditor": {
"undo": "Undo",
"redo": "Redo",
"save": "Save"
},
"extractionInProgress": "Extraction in progress",
"pasteInProgress": "Pasting in progress",
"deleteInProgress": "Deletion in progress"
},
"email": {
"backAction": "Back to Email",
@@ -1453,7 +1477,8 @@
"description": "If the server is running out of disk space, use this to move the app's data to a <a href=\"/#/volumes\">volume</a>. Any data here is part of the app's backup.",
"dataDirPlaceholder": "Leave empty to use platform default",
"moveAction": "Move Data",
"diskUsage": "The app is currently using {{ size }} of storage (as of {{ date }})."
"diskUsage": "The app is currently using {{ size }} of storage (as of {{ date }}).",
"mountTypeWarning": "The destination file system must support file permissions and ownership for the move to work"
},
"mounts": {
"title": "Mounts",
@@ -1697,6 +1722,17 @@
"label": "Label",
"clearIconAction": "Clear Icon",
"clearIconDescription": "This will try to fetch the app's favicon on save."
},
"servicesTabTitle": "Services",
"turn": {
"title": "TURN Setup",
"enable": "Configure the app to use the built-in TURN server",
"disable": "Do not configure the app's TURN settings. The app's TURN settings are left alone. You can configure it inside the app."
},
"redis": {
"title": "Redis Configuration",
"enable": "Configure the app to use Redis",
"disable": "Disable Redis"
}
},
"login": {
@@ -1879,5 +1915,6 @@
"newClient": "New client",
"empty": "No clients yet"
}
}
},
"automation": "Automation"
}
+63 -11
View File
@@ -102,7 +102,8 @@
"pagination": {
"perPageSelector": "Mostrar {{ n }} por página",
"next": "siguiente",
"prev": "anterior"
"prev": "anterior",
"itemCount": "Encontrado {{ count }}"
},
"table": {
"date": "Fecha"
@@ -115,7 +116,8 @@
"no": "No",
"close": "Cerrar",
"save": "Guardar",
"cancel": "Cancelar"
"cancel": "Cancelar",
"delete": "Borrar"
},
"logout": "Salir",
"offline": "Cloudron está desconectado. Reconectando…",
@@ -137,7 +139,9 @@
},
"enableAction": "Habilitar",
"statusEnabled": "Habilitado",
"statusDisabled": "Deshabilitado"
"statusDisabled": "Deshabilitado",
"loadingPlaceholder": "Cargando",
"settings": "Ajustes"
},
"apps": {
"domainsFilterHeader": "Todos los Dominios",
@@ -950,7 +954,11 @@
"vultrToken": "Token Vultr",
"jitsiHostname": "Ubicación de Jitsi",
"wellKnownDescription": "Cloudron utilizará los valores para responder a las URLs <code>/.well-known/</code> . Ten en cuenta que la aplicación debe estar disponible en el dominio desnudo <code>{{ domain }}</code> para que esto funcione. Consulta <a href=\"{{docsLink}}\" target=\"_blank\">esta documentación</a> para más información.",
"hetznerToken": "Token de Hetzner"
"hetznerToken": "Token de Hetzner",
"bunnyAccessKey": "Clave de acceso Bunny",
"cloudflareDefaultProxyStatus": "Habilitar proxy para nuevos registros DNS",
"porkbunApikey": "Clave API",
"porkbunSecretapikey": "Clave API secreta"
},
"subscriptionRequired": {
"setupAction": "Configura tu suscripción",
@@ -982,7 +990,8 @@
"domainWellKnown": {
"title": "Ubicaciones Well-known de {{ domain }}"
},
"tooltipWellKnown": "Establece las ubicaciones Well-Known"
"tooltipWellKnown": "Establece las ubicaciones Well-Known",
"count": "Dominios totales: {{ count }}"
},
"app": {
"appInfo": {
@@ -1098,7 +1107,8 @@
"saveAction": "Guardar",
"description": "La configuración de esta opción anulará cualquier encabezado CSP enviado por la propia aplicación",
"title": "Política de seguridad de contenido"
}
},
"hstsPreload": "Habilitar la carga previa de HSTS para este sitio y todos los subdominios"
},
"email": {
"from": {
@@ -1207,7 +1217,8 @@
"description": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una importación.",
"title": "Importar Backup",
"uploadAction": "Subir Configuración de Backup",
"importAction": "Importar"
"importAction": "Importar",
"remotePath": "Ruta del Backup"
},
"restoreDialog": {
"warning": "Todos los datos generados entre ahora y la última copia de seguridad conocida se perderán de forma irrevocable. Se recomienda crear una copia de seguridad de los datos actuales antes de intentar una restauración.",
@@ -1324,7 +1335,8 @@
"en": "Inglés",
"es": "Español",
"ru": "Ruso",
"pt": "Portugués"
"pt": "Portugués",
"da": "Danés"
},
"system": {
"title": "Información del Sistema",
@@ -1345,7 +1357,8 @@
"title": "Uso del Disco",
"usedInfo": "{{ used }} usados de {{ size }}",
"uninstalledApp": "Aplicación desinstalada",
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>"
"volumeContent": "Este disco es el volumen <code>{{ name }}</code>",
"diskSpeed": "Velocidad: {{ speed }} MB/seg"
},
"selectPeriodLabel": "Seleccionar Periodo"
},
@@ -1403,7 +1416,7 @@
"removeVolumeActionTooltip": "Borrar Volumen",
"openFileManagerActionTooltip": "Abrir Gestor de Archivos",
"name": "Nombre",
"hostPath": "Punto de montaje",
"hostPath": "Objetivo",
"addVolumeAction": "Añade un Volumen",
"title": "Volúmenes",
"description": "Los volúmenes son sistemas de archivos locales o remotos. Se pueden usar como el almacenamiento de datos principal de una aplicación o como una ubicación de almacenamiento compartida entre aplicaciones.",
@@ -1811,7 +1824,9 @@
"password": "Contraseña",
"2faToken": "Token 2FA (si está habilitado)",
"signInAction": "Iniciar sesión",
"resetPasswordAction": "Resetear contraseña"
"resetPasswordAction": "Resetear contraseña",
"errorIncorrect2FAToken": "El token 2FA es inválido",
"errorInternal": "Error interno, prueba de nuevo más tarde"
},
"newLoginEmail": {
"subject": "[<% = cloudron%>] Nuevo inicio de sesión en tu cuenta",
@@ -1827,5 +1842,42 @@
"mounts": {
"description": "Las aplicaciones pueden acceder a <a href=\"/#/volumes\">volúmenes</a> montados a través del directorio <code>/media/{volume name}</code>. Estos datos no están incluidos en la copia de seguridad de la aplicación."
}
},
"oidc": {
"newClientDialog": {
"title": "Añadir Cliente",
"description": "Agrega una nueva configuración de cliente de conexión de OpenID.",
"createAction": "Crear"
},
"client": {
"name": "Nombre",
"id": "ID de cliente",
"secret": "Secreto de cliente",
"signingAlgorithm": "Algoritmo de firma",
"loginRedirectUri": "URL de devolución de llamada de inicio de sesión (separadas por comas si hay más de una)",
"logoutRedirectUri": "URL de devolución de llamada de cierre de sesión (opcional)"
},
"title": "Proveedor de conexión OpenID",
"description": "Cloudron puede actuar como proveedor de OpenID Connect para aplicaciones internas y servicios externos.",
"editClientDialog": {
"title": "Editar cliente {{ client }}"
},
"deleteClientDialog": {
"title": "¿Realmente quieres borrar el cliente {{ client }}?",
"description": "Esto desconectará todas las aplicaciones OpenID externas de este Cloudron que utilicen este ID de cliente."
},
"env": {
"discoveryUrl": "URL de descubrimiento",
"logoutUrl": "URL de cierre de sesión",
"profileEndpoint": "Punto final del perfil",
"keysEndpoint": "Punto final de claves",
"tokenEndpoint": "Punto final del Token",
"authEndpoint": "Punto final de autenticación"
},
"clients": {
"title": "Clientes",
"newClient": "Nuevo cliente",
"empty": "No hay clientes aún"
}
}
}
+7 -3
View File
@@ -50,7 +50,8 @@
"pagination": {
"prev": "préc.",
"next": "suiv.",
"perPageSelector": "Afficher {{ n }} par page"
"perPageSelector": "Afficher {{ n }} par page",
"itemCount": "Trouvé {{ count }}"
},
"action": {
"logs": "Journaux",
@@ -85,7 +86,8 @@
"users": "Utilisateurs"
},
"disableAction": "Désactiver",
"enableAction": "Activer"
"enableAction": "Activer",
"loadingPlaceholder": "Chargement"
},
"users": {
"title": "Annuaire des utilisateurs",
@@ -1739,7 +1741,9 @@
"usageInfo": "{{ available | prettyDiskSize }}</b> sur <b>{{ size | prettyDiskSize }}</b> disponible(s)",
"mountedAt": "{{ filesystem }} <small>monté sur</small> {{ mountpoint }}",
"title": "Utilisation du disque",
"usedInfo": "{{ used }} utilisé de {{ size }}"
"usedInfo": "{{ used }} utilisé de {{ size }}",
"uninstalledApp": "Désinstaller App",
"diskSpeed": "Vitesse : {{ speed }} MB/sec"
},
"title": "Info système"
},
+98 -19
View File
@@ -38,7 +38,8 @@
"save": "Opslaan",
"close": "Sluiten",
"no": "Nee",
"yes": "Ja"
"yes": "Ja",
"delete": "Verwijder"
},
"username": "Gebruikersnaam",
"displayName": "Naam",
@@ -54,7 +55,8 @@
},
"action": {
"reboot": "Herstart",
"logs": "Logbestanden"
"logs": "Logbestanden",
"showLogs": "Toon logbestanden"
},
"clipboard": {
"copied": "Gekopieerd naar klembord",
@@ -87,7 +89,9 @@
"enableAction": "Inschakelen",
"statusEnabled": "Ingeschakeld",
"statusDisabled": "Uitgeschakeld",
"loadingPlaceholder": "Laden"
"loadingPlaceholder": "Laden",
"settings": "Instellingen",
"saveAction": "Opslaan"
},
"appstore": {
"title": "App Store",
@@ -660,7 +664,7 @@
"settings": {
"title": "Instellingen",
"info": "Deze instellingen zijn generiek voor alle domeinen.",
"location": "Mail server locatie",
"location": "Mail Server Locatie",
"maxMailSize": "Maximale e-mail grootte",
"spamFilter": "Spam filtering",
"spamFilterOverview": "{{ blacklistCount }} adres(sen) op de blokkeerlijst.",
@@ -708,7 +712,7 @@
"manualInfo": "Voeg handmatig een A record toe voor {{ domain }} die verwijst naar het IP van deze Cloudron",
"locationPlaceholder": "Leeg laten om hoofddomein te gebruiken",
"title": "E-mail server locatie aanpassen",
"description": "Cloudron zorgt voor de benodigde DNS aanpassingen van alle domeinen en herstart de e-mail server. Desktop & mobiele e-mailprogramma's moeten opnieuw geconfigureerd worden met deze nieuwe locatie als IMAP en SMTP server."
"description": "Dit verhuist de IMAP en SMTP server naar de aangegeven lokatie."
},
"changeMailSizeDialog": {
"title": "Maximale e-mail grootte aanpassen",
@@ -831,7 +835,7 @@
"cancelAction": "Annuleer",
"showLogsAction": "Toon logbestanden",
"title": "Dashboard-domein aanpassen",
"description": "Hierdoor verhuist het Dashboard en de e-mailserver naar het <code>my</code> subdomein van het geselecteerde domein."
"description": "Hierdoor verhuist het Dashboard naar het <code>my</code> subdomein van het geselecteerde domein."
},
"subscriptionRequired": {
"title": "Abonnement verplicht",
@@ -852,7 +856,8 @@
"domainWellKnown": {
"title": "Well-Known locaties van {{ domain }}"
},
"tooltipWellKnown": "Well-Known Locaties instellen"
"tooltipWellKnown": "Well-Known Locaties instellen",
"count": "Totaal domeinen: {{ count }}"
},
"app": {
"email": {
@@ -960,7 +965,8 @@
"dataDirPlaceholder": "Laat leeg om platformstandaard te gebruiken",
"moveAction": "Verplaats data",
"description": "Als de server onvoldoende schijfruimte heeft, gebruik dit om de app data te verplaatsen naar een <a href=\"/#/volumes\">volume</a>. Alle data daar is onderdeel van de app's backup.",
"diskUsage": "De app gebruikt momenteel {{ size }} aan opslag (sinds {{ date }})."
"diskUsage": "De app gebruikt momenteel {{ size }} aan opslag (sinds {{ date }}).",
"mountTypeWarning": "Het bestemmingsbestandssysteem moet bestandsmachtigingen en eigendom ondersteunen om de verhuizing te laten werken"
},
"mounts": {
"title": "Koppelpunten",
@@ -1164,7 +1170,7 @@
"service": "Dienst (start eenmalig)"
},
"title": "Crontab",
"saveAction": "Bewaar",
"saveAction": "Opslaan",
"addCommonPattern": "Voeg gemeenschappelijk patroon toe",
"description": "Eigen app-specifieke cron jobs kunnen hier toegevoegd worden. Let op: standaard cron jobs voor deze applicatie zijn al geïntegreerd in de app en hoef je hier niet te configureren."
},
@@ -1182,6 +1188,17 @@
"label": "Label",
"clearIconAction": "Icoon verwijderen",
"clearIconDescription": "Hiermee wordt geprobeerd de favicon van de app op te halen na opslaan."
},
"servicesTabTitle": "Diensten",
"turn": {
"title": "TURN Instellen",
"enable": "Configureer de app om de ingebouwde TURN server te gebruiken",
"disable": "Configureer de TURN-instellingen van de app niet. De TURN-instellingen van de app worden met rust gelaten. Je kunt het in de app configureren."
},
"redis": {
"title": "Redis configuratie",
"enable": "Configureer de app om Redis te gebruiken",
"disable": "Redis uitschakelen"
}
},
"network": {
@@ -1208,7 +1225,8 @@
},
"dyndns": {
"title": "Dynamische DNS",
"description": "Schakel deze optie in om je DNS records synchroon te houden met je veranderende IP adres. Dit is handig als je Cloudron opgenomen is in een netwerk waarbij het publieke IP adres steeds wisselt zoals in een thuissituatie."
"description": "Schakel deze optie in om je DNS records synchroon te houden met je veranderende IP adres. Dit is handig als je Cloudron opgenomen is in een netwerk waarbij het publieke IP adres steeds wisselt zoals in een thuissituatie.",
"showLogsAction": "Toon logbestanden"
},
"configureIp": {
"title": "Configureer IP aanbieder",
@@ -1224,7 +1242,13 @@
},
"configureIpv6": {
"title": "Configureer IPv6 aanbieder"
}
},
"trustedIps": {
"description": "HTTP headers van bijbehorende IP adressen worden vertrouwd",
"summary": "{{ trustCount }} IPs vertrouwd",
"title": "Configureer vertrouwde IPs"
},
"trustedIpRanges": "Vertrouwde IPs & bereiken "
},
"services": {
"title": "Diensten",
@@ -1393,7 +1417,9 @@
"logs": {
"title": "Logbestanden",
"clear": "Leegmaken",
"download": "Download volledige logbestanden"
"download": "Download volledige logbestanden",
"notFoundError": "Geen taak of app gevonden",
"logsGoneError": "Log bestand(en) niet gevonden"
},
"terminal": {
"title": "Terminal",
@@ -1491,7 +1517,8 @@
"paste": "Plakken",
"copy": "Kopiëren",
"cut": "Knippen",
"edit": "Bewerk"
"edit": "Bewerk",
"open": "Open"
},
"mtime": "Bewerkt"
},
@@ -1506,13 +1533,25 @@
},
"newDirectory": {
"errorAlreadyExists": "Bestaat al"
}
},
"uploader": {
"exitWarning": "Uploaden nog bezig. Weet je zeker dat je deze pagina wilt sluiten?",
"uploading": "Uploaden"
},
"extractionInProgress": "Bezig met uitpakken",
"textEditor": {
"undo": "Ongedaan maken",
"redo": "Opnieuw doen",
"save": "Opslaan"
},
"pasteInProgress": "Bezig met plakken",
"deleteInProgress": "Bezig met verwijderen"
},
"email": {
"backAction": "Terug naar e-mail",
"config": {
"title": "E-mailconfiguratie {{ domain }}",
"clientConfiguration": "Configureren E-mail clients"
"clientConfiguration": "Configureren E-mail programma's"
},
"incoming": {
"disableAction": "Uitschakelen",
@@ -1558,7 +1597,7 @@
"incomingPasswordUsage": "Wachtwoord van de eigenaar van de mailbox",
"enabled": "Cloudron e-mailserver is geconfigureerd voor inkomende e-mails voor dit domein.",
"disabled": "Cloudron e-mailserver ontvangt geen inkomende e-mails voor dit domein.",
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail clients in te stellen."
"howToConnectDescription": "Gebruik onderstaande gegevens om e-mail programma's in te stellen."
},
"outbound": {
"tabTitle": "Uitgaand",
@@ -1685,7 +1724,7 @@
"updateMailinglistDialog": {
"activeCheckbox": "Mailing-lijst is actief"
},
"howToConnectInfoModal": "Configureren e-mail clients",
"howToConnectInfoModal": "Configureren e-mail programma's",
"mailboxImportDialog": {
"title": "Importeer Mailboxen",
"description": "Upload een JSON of CSV bestand met een schema zoals beschreven in onze <a href=\"{{ docsLink }}\" target=\"_blank\">documentatie</a>.",
@@ -1703,7 +1742,9 @@
"password": "Wachtwoord",
"resetPasswordAction": "Herstel wachtwoord",
"2faToken": "2FA Token (indien ingeschakeld)",
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord"
"errorIncorrectCredentials": "Onjuiste gebruikersnaam of wachtwoord",
"errorIncorrect2FAToken": "2FA token is niet geldig",
"errorInternal": "Interne fout, probeer later opnieuw"
},
"passwordReset": {
"title": "Wachtwoord herstellen",
@@ -1837,5 +1878,43 @@
"mounts": {
"description": "Apps kunnen toegang krijgen tot <a href=\"/#/volumes\">volumes</a> via <code>/media/{volume name}</code> directory. Deze data is niet opgenomen in de app backup."
}
}
},
"oidc": {
"newClientDialog": {
"title": "Client toevoegen",
"description": "Nieuwe OpenID Connect client instellingen toevoegen.",
"createAction": "Aanmaken"
},
"client": {
"name": "Naam",
"id": "Client ID",
"secret": "Client geheim",
"signingAlgorithm": "Ondertekeningsalgoritme",
"loginRedirectUri": "Login callback URL (met komma gescheiden indien meer dan één)",
"logoutRedirectUri": "Logout callback URL (optioneel)"
},
"title": "OpenID Connect aanbieder",
"description": "Cloudron kan als een OpenID Connect aanbieder voor interne apps en externe diensten fungeren.",
"editClientDialog": {
"title": "Bewerk Client {{ client }}"
},
"deleteClientDialog": {
"title": "Weet je zeker dat je Client {{ client }} wilt verwijderen?",
"description": "Hiermee worden alle externe OpenID apps met dit Client ID losgekoppeld."
},
"env": {
"discoveryUrl": "Discovery URL",
"logoutUrl": "Logout URL",
"profileEndpoint": "Profiel Eindpunt",
"keysEndpoint": "Sleutels Eindpunt",
"tokenEndpoint": "Token Eindpunt",
"authEndpoint": "Auth Eindpunt"
},
"clients": {
"title": "Clients",
"newClient": "Nieuwe Client",
"empty": "Nog geen Clients"
}
},
"automation": "Automatisering"
}
+97 -16
View File
@@ -56,7 +56,8 @@
"save": "Сохранить",
"close": "Закрыть",
"no": "Нет",
"yes": "Да"
"yes": "Да",
"delete": "Удалить"
},
"username": "Имя пользователя",
"displayName": "Отображаемое имя",
@@ -72,7 +73,8 @@
},
"action": {
"reboot": "Перезагрузка",
"logs": "Логи"
"logs": "Логи",
"showLogs": "Показать логи"
},
"searchPlaceholder": "Поиск",
"multiselect": {
@@ -87,7 +89,9 @@
"enableAction": "Включить",
"statusEnabled": "Включено",
"statusDisabled": "Выключено",
"loadingPlaceholder": "Загрузка"
"loadingPlaceholder": "Загрузка",
"settings": "Настройки",
"saveAction": "Сохранить"
},
"appstore": {
"category": {
@@ -640,7 +644,8 @@
"moveAction": "Переместить данные",
"dataDirPlaceholder": "Оставьте пустым, чтобы сохранить настройку по умолчанию",
"description": "Если на диске заканчивается место, вы можете перенести данные приложения в <a href=\"/#/volumes\">том</a>. Любые данные по этому пути станут частью резервной копии приложения.",
"diskUsage": "Приложение использует {{ size }} хранилища (по состоянию на {{ date }})."
"diskUsage": "Приложение использует {{ size }} хранилища (по состоянию на {{ date }}).",
"mountTypeWarning": "Чтобы перемещение прошло успешно конечная файловая система должна поддерживать разрешения и права доступа к файлам"
},
"mounts": {
"volume": "Том",
@@ -833,6 +838,17 @@
"label": "Метка",
"clearIconAction": "Очистить иконку",
"clearIconDescription": "Это действие попытается загрузить favicon после сохранения."
},
"servicesTabTitle": "Службы",
"turn": {
"title": "Настроить TURN",
"enable": "Настроить использование встроенного TURN сервера в приложении",
"disable": "Не настраивать TURN сервер для данного приложения. Вы можете настроить его самостоятельно внутри самого приложения."
},
"redis": {
"title": "Настроить Redis",
"enable": "Настроить использование Redis в приложении",
"disable": "Отключить Redis"
}
},
"backups": {
@@ -1035,7 +1051,7 @@
},
"changeDomainDialog": {
"title": "Изменить расположение сервера электронной почты",
"description": "Cloudron внесет необходимые изменения в DNS во всех доменах и перезапустит почтовый сервер. Настольные и мобильные почтовые клиенты должны быть повторно настроены для использования нового расположения сервера IMAP и SMTP.",
"description": "Данное действие перенесёт IMAP и SMTP сервер в указанное расположение.",
"location": "Расположение",
"locationPlaceholder": "Оставьте пустым, чтобы использовать основной домен",
"manualInfo": "Вручную добавьте A запись для {{ domain }}, указав публичный IP Вашего Cloudron"
@@ -1118,7 +1134,8 @@
},
"dyndns": {
"title": "Динамический DNS",
"description": "Включите эту опцию, чтобы синхронизировать все ваши DNS-записи с изменяющимся IP-адресом. Это полезно, когда Cloudron работает в сети с часто меняющимся общедоступным IP-адресом, например, в домашних сетях."
"description": "Включите эту опцию, чтобы синхронизировать все ваши DNS-записи с изменяющимся IP-адресом. Это полезно, когда Cloudron работает в сети с часто меняющимся общедоступным IP-адресом, например, в домашних сетях.",
"showLogsAction": "Показать логи"
},
"configureIp": {
"title": "Настроить источник IP",
@@ -1134,7 +1151,13 @@
},
"configureIpv6": {
"title": "Настройка IPv6"
}
},
"trustedIps": {
"summary": "{{ trustCount }} IP доверены",
"title": "Настроить доверенные IP",
"description": "HTTP заголовки от совпадающих IP адресов будут доверены"
},
"trustedIpRanges": "Доверенные IP и диапазоны "
},
"services": {
"title": "Службы",
@@ -1310,7 +1333,7 @@
"changeAction": "Изменить домен",
"cancelAction": "Отменить",
"showLogsAction": "Показать логи",
"description": "Данное действие переместит панель управления и сервер электронной почты на <code>my</code> поддомен выбранного домена."
"description": "Данное действие переместит панель управления на <code>my</code> поддомен выбранного домена."
},
"subscriptionRequired": {
"title": "Требуется подписка",
@@ -1363,7 +1386,8 @@
"hetznerToken": "Токен Hetzner",
"cloudflareDefaultProxyStatus": "Активировать прокси для новых DNS записей",
"porkbunApikey": "API Ключ",
"porkbunSecretapikey": "Secret API Ключ"
"porkbunSecretapikey": "Secret API Ключ",
"bunnyAccessKey": "Ключ доступа Bunny"
},
"addDomain": "Добавить домен",
"removeDialog": {
@@ -1380,7 +1404,8 @@
"domainWellKnown": {
"title": "Общеизвестные расположения {{ domain }}"
},
"tooltipWellKnown": "Установить общеизвестные расположения"
"tooltipWellKnown": "Установить общеизвестные расположения",
"count": "Всего доменов: {{ count }}"
},
"notifications": {
"title": "Уведомления",
@@ -1392,7 +1417,9 @@
"logs": {
"title": "Логи",
"clear": "Очистить обзор",
"download": "Скачать полные логи"
"download": "Скачать полные логи",
"notFoundError": "Задача или приложение не существует",
"logsGoneError": "Файл(ы) журнала не найден(ы)"
},
"terminal": {
"title": "Терминал",
@@ -1477,7 +1504,8 @@
"cut": "Вырезать",
"paste": "Вставить",
"selectAll": "Выбрать все",
"copy": "Скопировать"
"copy": "Скопировать",
"open": "Открыть"
},
"symlink": "Символическая ссылка на {{ target }}",
"mtime": "Изменён"
@@ -1505,7 +1533,19 @@
},
"status": {
"restartingApp": "перезапускаем приложение"
}
},
"extractionInProgress": "Идёт извлечение",
"uploader": {
"exitWarning": "Загрузка ещё не завершена. Вы уверены, что хотите закрыть страницу?",
"uploading": "Загружаем"
},
"textEditor": {
"undo": "Отменить операцию",
"redo": "Повторить операцию",
"save": "Сохранить"
},
"pasteInProgress": "Выполняется копирование / перемещение",
"deleteInProgress": "Выполняется удаление"
},
"email": {
"outbound": {
@@ -1702,7 +1742,9 @@
"loginTo": "Войти в",
"username": "Имя пользователя",
"2faToken": "2FA Токен (если включен)",
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль"
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль",
"errorIncorrect2FAToken": "Неверный 2FA токен",
"errorInternal": "Внутренняя ошибка, попробуйте позже"
},
"passwordReset": {
"title": "Сброс пароля",
@@ -1776,7 +1818,8 @@
"zh_Hans": "Китайский (Упрощенный)",
"es": "Испанский",
"ru": "Русский",
"pt": "Португальский"
"pt": "Португальский",
"da": "Датский"
},
"setupAccount": {
"username": "Имя пользователя",
@@ -1835,5 +1878,43 @@
"mounts": {
"description": "Приложения могут получить доступ к смонтированным <a href=\"/#/volumes\">томам</a> по пути <code>/media/{имя тома}</code>. Данные таких томов не будут включаться в резервные копии приложения."
}
}
},
"oidc": {
"newClientDialog": {
"createAction": "Создать",
"title": "Добавить клиента",
"description": "Добавить настройки нового клиента OpenID connect."
},
"client": {
"name": "Имя",
"id": "ID Клиента",
"secret": "Секрет",
"signingAlgorithm": "Метод подписи",
"loginRedirectUri": "URL обратного вызова (если больше одного, отделите их запятой)",
"logoutRedirectUri": "URL обратного вызова для выхода из системы (необязательно)"
},
"clients": {
"title": "Клиенты",
"newClient": "Новый клиент",
"empty": "Клиенты не найдены"
},
"title": "Поставщик OpenID Сonnect",
"description": "Cloudron может выступать в качестве поставщика OpenID connect для внутренних приложений и внешних сервисов.",
"editClientDialog": {
"title": "Редактировать клиента {{ client }}"
},
"deleteClientDialog": {
"title": "Вы точно хотите удалить клиента {{ client }}?",
"description": "Это действие отключит все внешние OpenID приложения, использующие данный клиент ID, от Cloudron."
},
"env": {
"discoveryUrl": "URL обнаружения",
"logoutUrl": "URL выхода из системы",
"profileEndpoint": "Конечная точка профиля",
"keysEndpoint": "Конечная точка ключей",
"tokenEndpoint": "Конечная точка токена",
"authEndpoint": "Конечная точка аутентификации"
}
},
"automation": "Автоматизация"
}
+62 -3
View File
@@ -581,9 +581,9 @@
<i ng-hide="app.installationState === 'pending_start' || app.installationState === 'pending_stop'" class="fas" ng-class="{ 'fa-power-off': !uninstall.startButton, 'fa-play': uninstall.startButton }"></i>
</button>
<div class="btn-group btn-group-sm" role="group">
<a class="btn btn-sm btn-default" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
<a class="btn btn-sm btn-default" ng-if="app.type !== APP_TYPES.PROXIED" ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/filemanager.html?type=app&id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
<a class="btn btn-sm btn-default" ng-href="{{ '/frontend/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.logsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-align-left"></i></a>
<a class="btn btn-sm btn-default" ng-if="app.type !== APP_TYPES.PROXIED" ng-href="{{ '/frontend/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="{{ 'app.terminalActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fa fa-terminal"></i></a>
<a class="btn btn-sm btn-default" ng-if="app.manifest.addons.localstorage" ng-href="{{ '/frontend/filemanager.html#/home/app/' + app.id }}" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a>
</div>
<div class="dropdown" style="display: inline-block">
<button class="btn btn-sm btn-default dropdown-toggle" type="button" data-toggle="dropdown" uib-tooltip="{{ 'app.docsActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom">
@@ -632,6 +632,7 @@
<div ng-click="setView('proxy')" ng-class="{ 'active': view === 'proxy' }" ng-show="app.type === APP_TYPES.PROXIED">Proxy</div>
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access' }" ng-show="app.accessLevel === 'admin'">{{ 'app.accessControlTabTitle' | tr }}</div>
<div ng-click="setView('resources')" ng-class="{ 'active': view === 'resources' }" ng-show="app.type !== APP_TYPES.PROXIED">{{ 'app.resourcesTabTitle' | tr }}</div>
<div ng-click="setView('services')" ng-class="{ 'active': view === 'services' }" ng-show="app.type !== APP_TYPES.PROXIED && (app.manifest.addons.turn.optional || app.manifest.addons.redis.optional)">{{ 'app.servicesTabTitle' | tr }}</div>
<div ng-click="setView('storage')" ng-class="{ 'active': view === 'storage' }" ng-show="app.accessLevel === 'admin' && app.type !== APP_TYPES.PROXIED">{{ 'app.storageTabTitle' | tr }}</div>
<div ng-click="setView('graphs')" ng-class="{ 'active': view === 'graphs' }" ng-show="app.type !== APP_TYPES.PROXIED">{{ 'app.graphsTabTitle' | tr }}</div>
<div ng-click="setView('security')" ng-class="{ 'active': view === 'security' }">{{ 'app.securityTabTitle' | tr }}</div>
@@ -965,6 +966,61 @@
</div>
</div>
<div class="card" ng-show="view === 'services'">
<div class="row" ng-show="app.manifest.addons.turn.optional">
<div class="col-md-12">
<label class="control-label">{{ 'app.turn.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#turn" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div class="radio">
<label>
<input type="radio" ng-model="services.enableTurn" value="1"> {{ 'app.turn.enable' | tr }}
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="services.enableTurn" value="0"> {{ 'app.turn.disable' | tr }}
</label>
</div>
</div>
<div class="col-md-12 text-right">
<br/>
<button class="btn btn-outline btn-primary pull-right" ng-click="services.submitTurn()" ng-disabled="app.enableTurn === services.enableTurn || services.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="services.busy"></i> {{ 'main.saveAction' | tr }}
</button>
</div>
</div>
<hr ng-show="app.manifest.addons.turn.optional && app.manifest.addons.redis.optional">
<div class="row" ng-show="app.manifest.addons.redis.optional">
<div class="col-md-12">
<label class="control-label">{{ 'app.redis.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#redis" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div class="radio">
<label>
<input type="radio" ng-model="services.enableRedis" value="1"> {{ 'app.redis.enable' | tr }}
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="services.enableRedis" value="0"> {{ 'app.redis.disable' | tr }}
</label>
</div>
</div>
<div class="col-md-12 text-right">
<br/>
<button class="btn btn-outline btn-primary pull-right" ng-click="services.submitRedis()" ng-disabled="app.enablRedis === services.enableRedis || services.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="services.busy"></i> {{ 'main.saveAction' | tr }}
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'storage'">
<div class="row">
<div class="col-md-12">
@@ -973,6 +1029,7 @@
<p ng-bind-html="'app.storage.appdata.description' | tr:{ storagePath: ('/home/yellowtent/appsdata/' + app.id) }"></p>
<form role="form" name="storageDataDirForm" ng-submit="storage.submitDataDir()" autocomplete="off">
<select class="form-control" ng-model="storage.location" ng-options="location.displayName for location in storage.locationOptions track by location.id"></select>
<p class="text-warning" ng-show="storage.location.type === 'volume' && storage.location.mountType === 'mountpoint'" ng-bind-html="'app.storage.appdata.mountTypeWarning' | tr"></p>
<br/>
@@ -985,6 +1042,7 @@
<input class="ng-hide" type="submit" ng-disabled="!storageDataDirForm.$dirty || storageDataDirForm.$invalid || storage.busyDataDir || app.error || app.taskId"/>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
@@ -1021,6 +1079,7 @@
</select>
</td>
<td class="text-right no-wrap" style="vertical-align: middle">
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/filemanager.html#/home/volume/' + mount.volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
<button class="btn btn-danger btn-xs" ng-click="storage.delMount($event, $index)"><i class="far fa-trash-alt"></i></button>
</td>
</tr>
+49 -1
View File
@@ -585,6 +585,54 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
},
};
$scope.services = {
error: {},
busy: false,
enableTurn: '1', // curse of radio buttons
enableRedis: '1',
show: function () {
var app = $scope.app;
$scope.services.error = {};
$scope.services.enableTurn = app.enableTurn ? '1' : '0';
$scope.services.enableRedis = app.enableRedis ? '1' : '0';
},
submitTurn: function () {
$scope.services.busy = true;
$scope.services.error = {};
Client.configureApp($scope.app.id, 'turn', { enable: $scope.services.enableTurn === '1' }, function (error) {
if (error && error.statusCode === 400) {
$scope.services.busy = false;
$scope.services.error.turn = true;
return;
}
if (error) return Client.error(error);
$timeout(function () { $scope.services.busy = false; }, 1000);
});
},
submitRedis: function () {
$scope.services.busy = true;
$scope.services.error = {};
Client.configureApp($scope.app.id, 'redis', { enable: $scope.services.enableRedis === '1' }, function (error) {
if (error && error.statusCode === 400) {
$scope.services.busy = false;
$scope.services.error.redis = true;
return;
}
if (error) return Client.error(error);
$timeout(function () { $scope.services.busy = false; }, 1000);
});
},
};
$scope.storage = {
error: {},
@@ -613,7 +661,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
];
$scope.volumes.forEach(function (volume) {
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name });
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name, mountType: volume.mountType });
});
$scope.storage.location = $scope.storage.locationOptions.find(function (l) { return l.id === (app.storageVolumeId || 'default'); });
+6 -8
View File
@@ -187,21 +187,19 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
accessRestriction.groups = $scope.applinksEdit.accessRestriction.groups.map(function (g) { return g.id; });
}
var icon;
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
icon = '';
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
}
var data = {
upstreamUri: $scope.applinksEdit.upstreamUri,
label: $scope.applinksEdit.label,
accessRestriction: accessRestriction,
icon: icon,
tags: $scope.applinksEdit.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; })
};
if ($scope.applinksEdit.icon.data === '__original__') { // user reset the icon
data.icon = '';
} else if ($scope.applinksEdit.icon.data) { // user loaded custom icon
data.icon = $scope.applinksEdit.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
}
Client.updateApplink($scope.applinksEdit.id, data, function (error) {
$scope.applinksEdit.busyEdit = false;
+1 -1
View File
@@ -25,7 +25,6 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
$scope.cachedCategory = ''; // used to cache the selected category while searching
$scope.searchString = '';
$scope.validSubscription = false;
$scope.unstableApps = false;
$scope.subscription = {};
$scope.memory = null; // { memory, swap }
@@ -45,6 +44,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$tran
// If new categories added make sure the translation below exists
$scope.categories = [
{ id: 'analytics', icon: 'fa fa-chart-line', label: 'Analytics'},
{ id: 'automation', icon: 'fa fa-robot', label: 'Automation'},
{ id: 'blog', icon: 'fa fa-font', label: 'Blog'},
{ id: 'chat', icon: 'fa fa-comments', label: 'Chat'},
{ id: 'crm', icon: 'fab fa-connectdevelop', label: 'CRM'},
+59 -37
View File
@@ -101,47 +101,47 @@
<div class="modal-body">{{ 'backups.cleanupBackups.description' | tr }}</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="createBackup.startCleanup()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="cleanupBackups.start()">{{ 'backups.cleanupBackups.cleanupNow' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- modal backup config -->
<div class="modal fade" id="configureScheduleAndRetentionModal" tabindex="-1" role="dialog">
<!-- modal backup schedule config -->
<div class="modal fade" id="backupPolicyModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'backups.configureBackupSchedule.title' | tr }}</h4>
</div>
<div class="modal-body">
<form name="configureScheduleAndRetentionForm" role="form" novalidate ng-submit="configureScheduleAndRetention.submit()" autocomplete="off">
<p class="has-error text-center" ng-show="configureScheduleAndRetention.error">{{ configureScheduleAndRetention.error.generic }}</p>
<form name="backupPolicyForm" role="form" novalidate ng-submit="backupPolicy.submit()" autocomplete="off">
<p class="has-error text-center" ng-show="backupPolicy.error">{{ backupPolicy.error.generic }}</p>
<div class="form-group">
<label class="control-label" for="backupSchedule">{{ 'backups.configureBackupSchedule.schedule' | tr }}</label>
<p ng-bind-html="'backups.configureBackupSchedule.scheduleDescription' | tr"></p>
<div class="row" style="margin-left: 20px;">
<div class="col-md-5" ng-class="{ 'has-error': !configureScheduleAndRetention.days.length }">
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="configureScheduleAndRetention.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.days.length }">
{{ 'backups.configureBackupSchedule.days' | tr }}: <multiselect id="backupSchedule" class="input-sm stretch" ng-model="backupPolicy.days" options="a.name for a in cronDays" data-multiple="true" ng-required></multiselect>
</div>
<div class="col-md-5" ng-class="{ 'has-error': !configureScheduleAndRetention.hours.length }">
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="configureScheduleAndRetention.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
<div class="col-md-5" ng-class="{ 'has-error': !backupPolicy.hours.length }">
{{ 'backups.configureBackupSchedule.hours' | tr }}: <multiselect class="input-sm stretch" ng-model="backupPolicy.hours" options="a.name for a in cronHours" data-multiple="true"></multiselect>
</div>
</div>
</div>
<div class="form-group">
<label class="control-label" for="backupRetention">{{ 'backups.configureBackupSchedule.retentionPolicy' | tr }}</label>
<select class="form-control" id="backupRetention" ng-model="configureScheduleAndRetention.retentionPolicy" ng-options="a.value as a.name for a in retentionPolicies"></select>
<select class="form-control" id="backupRetention" ng-model="backupPolicy.retention" ng-options="a.value as a.name for a in backupRetentions"></select>
</div>
</form>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="configureScheduleAndRetention.submit()" ng-disabled="!configureScheduleAndRetention.valid() || configureScheduleAndRetention.busy"><i class="fa fa-circle-notch fa-spin" ng-show="configureScheduleAndRetention.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="backupPolicy.submit()" ng-disabled="!backupPolicy.valid() || backupPolicy.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backupPolicy.busy"></i><span> {{ 'main.dialog.save' | tr }}</span></button>
</div>
</div>
</div>
@@ -207,10 +207,16 @@
<input type="password" class="form-control" ng-model="configureBackup.mountOptions.password" id="configureBackupPassword" name="cifsPassword" ng-disabled="configureBackup.busy" password-reveal>
</div>
<!-- EXT4 -->
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'ext4' || configureBackup.provider === 'xfs'">
<!-- EXT4/XFS -->
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
<label class="control-label" for="inputConfigureDiskPath">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'ext4' || configureBackup.provider === 'xfs'">
<input type="text" class="form-control" ng-model="configureBackup.mountOptions.diskPath" id="inputConfigureDiskPath" name="diskPath" ng-disabled="configureBackup.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="configureBackup.provider === 'xfs' || configureBackup.provider === 'ext4'">
</div>
<!-- Disk -->
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.diskPath || !configureBackup.mountOptions.diskPath }" ng-show="configureBackup.provider === 'disk'">
<label class="control-label">{{ 'backups.configureBackupStorage.diskPath' | tr }}</label>
<select class="form-control" ng-model="configureBackup.disk" ng-options="item as item.label for item in configureBackup.blockDevices track by item.path" ng-required="configureBackup.provider === 'disk'"></select>
</div>
<!-- SSHFS -->
@@ -468,8 +474,8 @@
<div class="col-xs-6 text-right no-wrap">
<span ng-show="backupConfig.provider === 'filesystem'">{{ backupConfig.backupFolder }}</span>
<span ng-show="mountlike(backupConfig.provider)">
<i class="fa fa-circle" ng-style="{ color: backupConfig.mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="backupConfig.mountStatus" uib-tooltip="{{ backupConfig.mountStatus.message }}"></i>
<span ng-show="backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
<i class="fa fa-circle" ng-style="{ color: mountStatus.state === 'active' ? '#27CE65' : '#d9534f' }" ng-show="mountStatus" uib-tooltip="{{ mountStatus.message }}"></i>
<span ng-show="backupConfig.provider === 'disk' || backupConfig.provider === 'filesystem' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'mountpoint'">{{ backupConfig.mountOptions.diskPath || backupConfig.mountPoint }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
<span ng-show="backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs'">{{ backupConfig.mountOptions.host }}:{{ backupConfig.mountOptions.remoteDir }}{{ (backupConfig.prefix ? '/' : '') + backupConfig.prefix }}</span>
</span>
@@ -507,8 +513,23 @@
</div>
</div>
<div class="text-left">
<h3>{{ 'backups.schedule.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'backups.schedule.title' | tr }}
<!-- <a class="btn btn-sm btn-default pull-right" ng-href="/frontend/logs.html?taskId={{cleanupBackups.taskId}}" target="_blank" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}"><i class="fas fa-align-left"></i></a> -->
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="cleanupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in cleanupTasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card" style="margin-bottom: 15px;">
@@ -518,7 +539,7 @@
<span class="text-muted">{{ 'backups.schedule.schedule' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ prettyBackupSchedule(backupConfig.schedulePattern) }}</span>
<span>{{ prettyBackupSchedule(backupPolicy.currentPolicy.schedule) }}</span>
</div>
</div>
<div class="row">
@@ -526,19 +547,34 @@
<span class="text-muted">{{ 'backups.schedule.retentionPolicy' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ prettyBackupRetentionPolicy(backupConfig.retentionPolicy) }}</span>
<span>{{ prettyBackupRetention(backupPolicy.currentPolicy.retention) }}</span>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="configureScheduleAndRetention.show()">{{ 'backups.schedule.configure' | tr }}</button>
<button class="btn btn-default" ng-click="cleanupBackups.ask()" ng-disabled="cleanupBackups.busy" style="margin-right: 5px"><i class="fa fa-circle-notch fa-spin" ng-show="cleanupBackups.busy"></i> {{ 'backups.listing.cleanupBackups' | tr }}</button>
<button class="btn btn-outline btn-primary pull-right" ng-show="user.isAtLeastOwner" ng-click="backupPolicy.show()">{{ 'backups.schedule.configure' | tr }}</button>
</div>
</div>
</div>
<div class="text-left">
<h3>{{ 'backups.listing.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'backups.listing.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="backupTasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'backups.logs.showLogs' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in backupTasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card card-large">
@@ -594,23 +630,9 @@
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-default" ng-click="createBackup.cleanupBackups()" ng-show="!createBackup.busy" style="margin-right: 5px">{{ 'backups.listing.cleanupBackups' | tr }}</button>
<button class="btn btn-outline btn-primary" ng-click="createBackup.startBackup()" ng-show="!createBackup.busy">{{ 'backups.listing.backupNow' | tr }}</button>
<button class="btn btn-outline btn-danger" ng-click="createBackup.stopTask()" ng-show="createBackup.busy">{{ 'backups.listing.stopTask' | tr:{ taskType: createBackup.taskType } }}</button>
</div>
</div>
</div>
<div class="text-left">
<h3>{{ 'backups.logs.title' | tr }}</h3>
</div>
<div class="card card-large" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
<p>{{ 'backups.logs.description' | tr }}</p>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{createBackup.taskId}}" ng-disabled="!createBackup.taskId" target="_blank">{{ 'backups.logs.showLogs' | tr }}</a>
</div>
</div>
</div>
</div>
+191 -77
View File
@@ -12,11 +12,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.memory = null; // { memory, swap }
$scope.mountStatus = null; // { state, message }
$scope.manualBackupApps = [];
$scope.backupConfig = {};
$scope.backups = [];
$scope.backupTasks = [];
$scope.cleanupTasks = [];
$scope.s3Regions = REGIONS_S3;
$scope.wasabiRegions = REGIONS_WASABI;
@@ -33,7 +35,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
{ name: 'No-op (Only for testing)', value: 'noop' }
]);
$scope.retentionPolicies = [
$scope.backupRetentions = [
{ name: '2 days', value: { keepWithinSecs: 2 * 24 * 60 * 60 }},
{ name: '1 week', value: { keepWithinSecs: 7 * 24 * 60 * 60 }}, // default
{ name: '1 month', value: { keepWithinSecs: 30 * 24 * 60 * 60 }},
@@ -83,8 +85,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
return prettyDay + ' at ' + prettyHour;
};
$scope.prettyBackupRetentionPolicy = function (retentionPolicy) {
var tmp = $scope.retentionPolicies.find(function (p) { return angular.equals(p.value, retentionPolicy); });
$scope.prettyBackupRetention = function (retention) {
var tmp = $scope.backupRetentions.find(function (p) { return angular.equals(p.value, retention); });
return tmp ? tmp.name : '';
};
@@ -119,11 +121,9 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
message: '',
errorMessage: '',
taskId: '',
taskType: TASK_TYPES.TASK_BACKUP,
checkStatus: function () {
// TODO support both task types TASK_BACKUP and TASK_CLEAN_BACKUPS
Client.getLatestTaskByType($scope.createBackup.taskType, function (error, task) {
init: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_BACKUP, function (error, task) {
if (error) return console.error(error);
if (!task) return;
@@ -143,6 +143,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.createBackup.percent = 100; // indicates that 'result' is valid
$scope.createBackup.errorMessage = data.success ? '' : data.error.message;
getBackupTasks();
return fetchBackups();
}
@@ -158,7 +160,6 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.createBackup.percent = 0;
$scope.createBackup.message = '';
$scope.createBackup.errorMessage = '';
$scope.createBackup.taskType = TASK_TYPES.TASK_BACKUP;
Client.startBackup(function (error, taskId) {
if (error) {
@@ -177,32 +178,14 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
return;
}
$scope.createBackup.taskId = taskId;
$scope.createBackup.updateStatus();
});
},
cleanupBackups: function () {
$('#cleanupBackupsModal').modal('show');
},
startCleanup: function () {
$scope.createBackup.busy = true;
$scope.createBackup.percent = 0;
$scope.createBackup.message = '';
$scope.createBackup.errorMessage = '';
$scope.createBackup.taskType = TASK_TYPES.TASK_CLEAN_BACKUPS;
$('#cleanupBackupsModal').modal('hide');
Client.cleanupBackups(function (error, taskId) {
if (error) console.error(error);
getBackupTasks();
$scope.createBackup.taskId = taskId;
$scope.createBackup.updateStatus();
});
},
stopTask: function () {
Client.stopTask($scope.createBackup.taskId, function (error) {
if (error) {
@@ -214,6 +197,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
}
$scope.createBackup.busy = false;
getBackupTasks();
return;
}
@@ -221,6 +205,62 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
}
};
$scope.cleanupBackups = {
busy: false,
taskId: 0,
init: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.cleanupBackups.taskId = task.id;
$scope.cleanupBackups.updateStatus();
getCleanupTasks();
});
},
updateStatus: function () {
Client.getTask($scope.cleanupBackups.taskId, function (error, data) {
if (error) return window.setTimeout($scope.cleanupBackups.updateStatus, 5000);
if (!data.active) {
$scope.cleanupBackups.busy = false;
getCleanupTasks();
fetchBackups();
return;
}
$scope.cleanupBackups.busy = true;
$scope.cleanupBackups.message = data.message;
window.setTimeout($scope.cleanupBackups.updateStatus, 3000);
});
},
ask: function () {
$('#cleanupBackupsModal').modal('show');
},
start: function () {
$scope.cleanupBackups.busy = true;
$('#cleanupBackupsModal').modal('hide');
Client.cleanupBackups(function (error, taskId) {
if (error) console.error(error);
$scope.cleanupBackups.taskId = taskId;
$scope.cleanupBackups.updateStatus();
getCleanupTasks();
});
}
};
$scope.listBackups = {
};
@@ -233,7 +273,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
};
$scope.mountlike = function (provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
};
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
@@ -261,7 +301,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
});
var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + '.json';
var filename = 'cloudron-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.config.adminFqdn + ')' + '.json';
download(filename, JSON.stringify(tmp, null, 4));
};
@@ -307,68 +347,76 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
}
};
$scope.configureScheduleAndRetention = {
$scope.backupPolicy = {
busy: false,
error: {},
retentionPolicy: $scope.retentionPolicies[0],
currentPolicy: null,
retention: null,
days: [],
hours: [],
init: function () {
Client.getBackupPolicy(function (error, policy) {
if (error) Client.error(error);
$scope.backupPolicy.currentPolicy = policy;
});
},
show: function () {
$scope.configureScheduleAndRetention.error = {};
$scope.configureScheduleAndRetention.busy = false;
$scope.backupPolicy.error = {};
$scope.backupPolicy.busy = false;
var selectedPolicy = $scope.retentionPolicies.find(function (x) { return angular.equals(x.value, $scope.backupConfig.retentionPolicy); });
if (!selectedPolicy) selectedPolicy = $scope.retentionPolicies[0];
var selectedRetention = $scope.backupRetentions.find(function (x) { return angular.equals(x.value, $scope.backupPolicy.currentPolicy.retention); });
if (!selectedRetention) selectedRetention = $scope.backupRetentions[0];
$scope.configureScheduleAndRetention.retentionPolicy = selectedPolicy.value;
$scope.backupPolicy.retention = selectedRetention.value;
var tmp = $scope.backupConfig.schedulePattern.split(' ');
var tmp = $scope.backupPolicy.currentPolicy.schedule.split(' ');
var hours = tmp[2].split(','), days = tmp[5].split(',');
if (days[0] === '*') {
$scope.configureScheduleAndRetention.days = angular.copy($scope.cronDays, []);
$scope.backupPolicy.days = angular.copy($scope.cronDays, []);
} else {
$scope.configureScheduleAndRetention.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
$scope.backupPolicy.days = days.map(function (day) { return $scope.cronDays[parseInt(day, 10)]; });
}
$scope.configureScheduleAndRetention.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
$scope.backupPolicy.hours = hours.map(function (hour) { return $scope.cronHours[parseInt(hour, 10)]; });
$('#configureScheduleAndRetentionModal').modal('show');
$('#backupPolicyModal').modal('show');
},
valid: function () {
return $scope.configureScheduleAndRetention.days.length && $scope.configureScheduleAndRetention.hours.length;
return $scope.backupPolicy.days.length && $scope.backupPolicy.hours.length;
},
submit: function () {
if (!$scope.configureScheduleAndRetention.days.length) return;
if (!$scope.configureScheduleAndRetention.hours.length) return;
if (!$scope.backupPolicy.days.length) return;
if (!$scope.backupPolicy.hours.length) return;
$scope.configureScheduleAndRetention.error = {};
$scope.configureScheduleAndRetention.busy = true;
// start with the full backupConfig since the api requires all fields
var backupConfig = $scope.backupConfig;
backupConfig.retentionPolicy = $scope.configureScheduleAndRetention.retentionPolicy;
$scope.backupPolicy.error = {};
$scope.backupPolicy.busy = true;
var daysPattern;
if ($scope.configureScheduleAndRetention.days.length === 7) daysPattern = '*';
else daysPattern = $scope.configureScheduleAndRetention.days.map(function (d) { return d.value; });
if ($scope.backupPolicy.days.length === 7) daysPattern = '*';
else daysPattern = $scope.backupPolicy.days.map(function (d) { return d.value; });
var hoursPattern;
if ($scope.configureScheduleAndRetention.hours.length === 24) hoursPattern = '*';
else hoursPattern = $scope.configureScheduleAndRetention.hours.map(function (d) { return d.value; });
if ($scope.backupPolicy.hours.length === 24) hoursPattern = '*';
else hoursPattern = $scope.backupPolicy.hours.map(function (d) { return d.value; });
backupConfig.schedulePattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern;
var policy = {
retention: $scope.backupPolicy.retention,
schedule: '00 00 ' + hoursPattern + ' * * ' + daysPattern
};
Client.setBackupConfig(backupConfig, function (error) {
$scope.configureScheduleAndRetention.busy = false;
Client.setBackupPolicy(policy, function (error) {
$scope.backupPolicy.busy = false;
if (error) {
if (error.statusCode === 424) {
$scope.configureScheduleAndRetention.error.generic = error.message;
$scope.backupPolicy.error.generic = error.message;
} else if (error.statusCode === 400) {
$scope.configureScheduleAndRetention.error.generic = error.message;
$scope.backupPolicy.error.generic = error.message;
} else {
console.error('Unable to change schedule or retention.', error);
}
@@ -376,13 +424,18 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
return;
}
$('#configureScheduleAndRetentionModal').modal('hide');
$('#backupPolicyModal').modal('hide');
getBackupConfig();
$scope.backupPolicy.init();
});
}
};
$scope.$watch('configureBackup.disk', function (newValue) {
if (!newValue) return;
$scope.configureBackup.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid;
});
$scope.configureBackup = {
busy: false,
error: {},
@@ -414,6 +467,8 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
downloadConcurrency: '',
syncConcurrency: '', // sort of similar to upload
blockDevices: [],
disk: null,
mountOptions: {
host: '',
remoteDir: '',
@@ -448,6 +503,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.configureBackup.syncConcurrency = $scope.configureBackup.provider === 's3' ? 20 : 10;
$scope.configureBackup.copyConcurrency = $scope.configureBackup.provider === 's3' ? 500 : 10;
$scope.configureBackup.disk = null;
$scope.configureBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: false, user: '', port: 22, privateKey: '' };
},
@@ -482,12 +538,13 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.configureBackup.useHardlinks = !$scope.backupConfig.noHardlinks;
$scope.configureBackup.chown = $scope.backupConfig.chown;
$scope.configureBackup.memoryLimit = $scope.backupConfig.memoryLimit;
var limits = $scope.backupConfig.limits || {};
$scope.configureBackup.memoryLimit = limits.memoryLimit;
$scope.configureBackup.uploadPartSize = $scope.backupConfig.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
$scope.configureBackup.downloadConcurrency = $scope.backupConfig.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
$scope.configureBackup.syncConcurrency = $scope.backupConfig.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
$scope.configureBackup.copyConcurrency = $scope.backupConfig.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
$scope.configureBackup.uploadPartSize = limits.uploadPartSize || ($scope.configureBackup.provider === 'scaleway-objectstorage' ? 100 * 1024 * 1024 : 10 * 1024 * 1024);
$scope.configureBackup.downloadConcurrency = limits.downloadConcurrency || ($scope.backupConfig.provider === 's3' ? 30 : 10);
$scope.configureBackup.syncConcurrency = limits.syncConcurrency || ($scope.backupConfig.provider === 's3' ? 20 : 10);
$scope.configureBackup.copyConcurrency = limits.copyConcurrency || ($scope.backupConfig.provider === 's3' ? 500 : 10);
var totalMemory = Math.max(($scope.memory.memory + $scope.memory.swap) * 1.5, 2 * 1024 * 1024);
$scope.configureBackup.memoryTicks = [ $scope.MIN_MEMORY_LIMIT ];
@@ -513,7 +570,28 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
privateKey: mountOptions.privateKey || ''
};
$('#configureBackupModal').modal('show');
Client.getBlockDevices(function (error, result) {
if (error) return console.error('Failed to list blockdevices:', error);
// only offer non /, /boot or /home disks
result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; });
// only offer xfs and ext4 disks
result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; });
// amend label for UI
result.forEach(function (d) {
d.label = d.path;
// pre-select current if set
if (d.path === $scope.configureBackup.mountOptions.diskPath || ('/dev/disk/by-uuid/' + d.uuid) === $scope.configureBackup.mountOptions.diskPath) {
$scope.configureBackup.disk = d;
}
});
$scope.configureBackup.blockDevices = result;
$('#configureBackupModal').modal('show');
});
},
submit: function () {
@@ -523,10 +601,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
var backupConfig = {
provider: $scope.configureBackup.provider,
format: $scope.configureBackup.format,
memoryLimit: $scope.configureBackup.memoryLimit,
// required for api call to provide all fields
schedulePattern: $scope.backupConfig.schedulePattern,
retentionPolicy: $scope.backupConfig.retentionPolicy
retentionPolicy: $scope.backupConfig.retentionPolicy,
limits: {
memoryLimit: $scope.configureBackup.memoryLimit,
},
};
if ($scope.configureBackup.password) {
backupConfig.password = $scope.configureBackup.password;
@@ -615,7 +695,7 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
backupConfig.mountOptions.port = $scope.configureBackup.mountOptions.port;
backupConfig.mountOptions.privateKey = $scope.configureBackup.mountOptions.privateKey;
}
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
} else if (backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs' || backupConfig.provider === 'disk') {
backupConfig.mountOptions.diskPath = $scope.configureBackup.mountOptions.diskPath;
} else if (backupConfig.provider === 'mountpoint') {
backupConfig.mountPoint = $scope.configureBackup.mountPoint;
@@ -627,12 +707,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
backupConfig.noHardlinks = !$scope.configureBackup.useHardlinks;
}
backupConfig.uploadPartSize = $scope.configureBackup.uploadPartSize;
backupConfig.limits.uploadPartSize = $scope.configureBackup.uploadPartSize;
if (backupConfig.format === 'rsync') {
backupConfig.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
backupConfig.syncConcurrency = $scope.configureBackup.syncConcurrency;
backupConfig.copyConcurrency = $scope.configureBackup.copyConcurrency;
backupConfig.limits.downloadConcurrency = $scope.configureBackup.downloadConcurrency;
backupConfig.limits.syncConcurrency = $scope.configureBackup.syncConcurrency;
backupConfig.limits.copyConcurrency = $scope.configureBackup.copyConcurrency;
}
Client.setBackupConfig(backupConfig, function (error) {
@@ -727,6 +807,35 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
if (error) return console.error(error);
$scope.backupConfig = backupConfig;
$scope.mountStatus = null;
if (!$scope.mountlike($scope.backupConfig.provider)) return;
Client.getBackupMountStatus(function (error, mountStatus) {
if (error) return console.error(error);
$scope.mountStatus = mountStatus;
});
});
}
function getBackupTasks() {
Client.getTasksByType(TASK_TYPES.TASK_BACKUP, function (error, tasks) {
if (error) return console.error(error);
if (!tasks.length) return;
$scope.backupTasks = tasks.slice(0, 10);
});
}
function getCleanupTasks() {
Client.getTasksByType(TASK_TYPES.TASK_CLEAN_BACKUPS, function (error, tasks) {
if (error) return console.error(error);
if (!tasks.length) return;
$scope.cleanupTasks = tasks.slice(0, 10);
});
}
@@ -742,7 +851,12 @@ angular.module('Application').controller('BackupsController', ['$scope', '$locat
$scope.manualBackupApps = Client.getInstalledApps().filter(function (app) { return app.type !== APP_TYPES.LINK && !app.enableBackup; });
// show backup status
$scope.createBackup.checkStatus();
$scope.createBackup.init();
$scope.cleanupBackups.init();
$scope.backupPolicy.init();
getBackupTasks();
getCleanupTasks();
});
});
+61 -22
View File
@@ -109,6 +109,14 @@
</div>
<!-- Cloudflare -->
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
</select>
</div>
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'GlobalApiKey'">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</label>
<label class="control-label" ng-show="domainConfigure.cloudflareTokenType === 'ApiToken'">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</label>
@@ -126,14 +134,6 @@
</label>
</div>
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'cloudflare'">
<label class="control-label">{{ 'domains.domainDialog.cloudflareTokenType' | tr }}</label>
<select class="form-control" ng-model="domainConfigure.cloudflareTokenType">
<option value="GlobalApiKey">{{ 'domains.domainDialog.cloudflareTokenTypeGlobalApiKey' | tr }}</option>
<option value="ApiToken">{{ 'domains.domainDialog.cloudflareTokenTypeApiToken' | tr }}</option>
</select>
</div>
<!-- Linode -->
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="domainConfigure.provider === 'linode'">
<label class="control-label">{{ 'domains.domainDialog.linodeToken' | tr }}</label>
@@ -331,9 +331,9 @@
{{ prettyProviderName(domain) }}
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" title="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" title="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" title="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="domainWellKnown.show(domain)" uib-tooltip="{{ 'domains.tooltipWellKnown' | tr }}"><i class="fa fa-atlas"></i></button>
<button class="btn btn-xs btn-default" ng-click="domainConfigure.show(domain)" uib-tooltip="{{ 'domains.tooltipEdit' | tr }}"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-danger" ng-click="domainRemove.show(domain)" uib-tooltip="{{ 'domains.tooltipRemove' | tr }}" ng-disabled="domain.domain === config.adminDomain"><i class="far fa-trash-alt"></i></button>
</td>
</tr>
</tbody>
@@ -350,8 +350,22 @@
</div>
</div>
<div class="text-left">
<h3>{{ 'domains.renewCerts.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'domains.renewCerts.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="renewCerts.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in renewCerts.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card">
@@ -375,14 +389,27 @@
<p ng-hide="renewCerts.busy">
<div class="has-error" ng-show="!renewCerts.active">{{ renewCerts.errorMessage }}</div>
</p>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{renewCerts.taskId}}" ng-disabled="!renewCerts.taskId" target="_blank">{{ 'domains.renewCerts.showLogsAction' | tr }}</a>
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy" style="margin-right: 10px">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
<button class="btn btn-outline btn-primary pull-right" ng-click="renewCerts.renew()" ng-disabled="renewCerts.busy">{{ 'domains.renewCerts.renewAllAction' | tr }}</button>
</div>
</div>
</div>
<div class="text-left">
<h3>{{ 'domains.syncDns.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'domains.syncDns.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="syncDns.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in syncDns.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card">
@@ -406,14 +433,27 @@
<p ng-hide="syncDns.busy">
<div class="has-error" ng-show="!syncDns.active">{{ syncDns.errorMessage }}</div>
</p>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{syncDns.taskId}}" ng-disabled="!syncDns.taskId" target="_blank">{{ 'domains.syncDns.showLogsAction' | tr }}</a>
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy" style="margin-right: 10px">{{ 'domains.syncDns.syncAction' | tr }}</button>
<button class="btn btn-outline btn-primary pull-right" ng-click="syncDns.sync()" ng-disabled="syncDns.busy">{{ 'domains.syncDns.syncAction' | tr }}</button>
</div>
</div>
</div>
<div class="text-left">
<h3>{{ 'domains.changeDashboardDomain.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'domains.changeDashboardDomain.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="changeDashboard.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'domains.renewCerts.showLogsAction' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in changeDashboard.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card">
@@ -447,7 +487,6 @@
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary" ng-click="changeDashboard.change()" ng-hide="changeDashboard.busy" ng-disabled="changeDashboard.selectedDomain.domain === changeDashboard.adminDomain.domain">{{ 'domains.changeDashboardDomain.changeAction' | tr }}</button>
<button class="btn btn-outline btn-danger" ng-click="changeDashboard.stop()" ng-show="changeDashboard.busy" style="margin-right: 10px">{{ 'domains.changeDashboardDomain.cancelAction' | tr }}</button>
<a class="btn btn-primary pull-right" ng-href="/logs.html?taskId={{changeDashboard.taskId}}" ng-show="changeDashboard.busy" target="_blank">{{ 'domains.changeDashboardDomain.showLogsAction' | tr }}</a>
</div>
</div>
</div>
+38 -42
View File
@@ -11,7 +11,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.domains = [];
$scope.ready = false;
$scope.domainSearchString = '';
$scope.pageSize = 10;
$scope.pageSize = localStorage.cloudronPageSize || 10;
$scope.currentPage = 1;
$scope.showNextPage = function () {
@@ -489,21 +489,20 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
percent: 0,
message: '',
errorMessage: '',
taskId: '',
tasks: [],
checkStatus: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, task) {
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_CHECK_CERTS, function (error, tasks) {
if (error) return console.error(error);
if (!task) return;
$scope.renewCerts.taskId = task.id;
$scope.renewCerts.updateStatus();
$scope.renewCerts.tasks = tasks.slice(0, 10);
if ($scope.renewCerts.tasks.length && $scope.renewCerts.tasks[0].active) $scope.renewCerts.updateStatus();
});
},
updateStatus: function () {
Client.getTask($scope.renewCerts.taskId, function (error, data) {
var taskId = $scope.renewCerts.tasks[0].id;
Client.getTask(taskId, function (error, data) {
if (error) return window.setTimeout($scope.renewCerts.updateStatus, 5000);
if (!data.active) {
@@ -512,6 +511,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.renewCerts.percent = 100; // indicates that 'result' is valid
$scope.renewCerts.errorMessage = data.success ? '' : data.error.message;
$scope.renewCerts.refreshTasks(); // update the tasks list dropdown
return;
}
@@ -529,15 +530,13 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.renewCerts.errorMessage = '';
// always rebuild the nginx configs when triggered via the UI. we assume user is clicking this because something is wrong
Client.renewCerts({ rebuild: true }, function (error, taskId) {
Client.renewCerts({ rebuild: true }, function (error /*, taskId */) {
if (error) {
console.error(error);
$scope.renewCerts.errorMessage = error.message;
$scope.renewCerts.busy = false;
} else {
$scope.renewCerts.taskId = taskId;
$scope.renewCerts.updateStatus();
$scope.renewCerts.refreshTasks();
}
});
}
@@ -548,21 +547,19 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
percent: 0,
message: '',
errorMessage: '',
taskId: '',
tasks: [],
checkStatus: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, task) {
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DNS_RECORDS, function (error, tasks) {
if (error) return console.error(error);
if (!task) return;
$scope.syncDns.taskId = task.id;
$scope.syncDns.updateStatus();
$scope.syncDns.tasks = tasks.slice(0, 10);
if ($scope.syncDns.tasks.length && $scope.syncDns.tasks[0].active) $scope.syncDns.updateStatus();
});
},
updateStatus: function () {
Client.getTask($scope.syncDns.taskId, function (error, data) {
var taskId = $scope.syncDns.tasks[0].id;
Client.getTask(taskId, function (error, data) {
if (error) return window.setTimeout($scope.syncDns.updateStatus, 5000);
if (!data.active) {
@@ -571,6 +568,8 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.syncDns.percent = 100; // indicates that 'result' is valid
$scope.syncDns.errorMessage = data.success ? '' : data.error.message;
$scope.syncDns.refreshTasks(); // update the tasks list dropdown
return;
}
@@ -587,15 +586,13 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.syncDns.message = '';
$scope.syncDns.errorMessage = '';
Client.setDnsRecords({}, function (error, taskId) {
Client.setDnsRecords({}, function (error /*, taskId */) {
if (error) {
console.error(error);
$scope.syncDns.errorMessage = error.message;
$scope.syncDns.busy = false;
} else {
$scope.syncDns.taskId = taskId;
$scope.syncDns.updateStatus();
$scope.syncDns.refreshTasks();
}
});
}
@@ -649,24 +646,21 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
taskId: '',
selectedDomain: null,
adminDomain: null,
tasks: [],
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_PREPARE_DASHBOARD_LOCATION, function (error, tasks) {
if (error) return console.error(error);
$scope.changeDashboard.tasks = tasks.slice(0, 10);
if ($scope.changeDashboard.tasks.length && $scope.changeDashboard.tasks[0].active) $scope.changeDashboard.updateStatus();
});
},
stop: function () {
Client.stopTask($scope.changeDashboard.taskId, function (error) {
if (error) console.error(error);
$scope.changeDashboard.busy = false;
});
},
// this function is not called intentionally. currently, we do switching in two steps - prepare and set
// if the user refreshed the UI in the middle of prepare, then it would be awkward to resume/call 'set' when the
// user visits the UI the next time around.
checkStatus: function () {
Client.getLatestTaskByType('prepareDashboardDomain', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.changeDashboard.taskId = task.id;
$scope.changeDashboard.updateStatus();
$scope.changeDashboard.refreshTasks();
});
},
@@ -721,7 +715,7 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.changeDashboard.busy = false;
} else {
$scope.changeDashboard.taskId = taskId;
$scope.changeDashboard.updateStatus();
$scope.changeDashboard.refreshTasks();
}
});
}
@@ -734,7 +728,9 @@ angular.module('Application').controller('DomainsController', ['$scope', '$locat
$scope.ready = true;
});
$scope.renewCerts.checkStatus();
$scope.renewCerts.refreshTasks();
$scope.syncDns.refreshTasks();
$scope.changeDashboard.refreshTasks();
});
document.getElementById('gcdnsKeyFileInput').onchange = readFileLocally($scope.domainConfigure.gcdnsKey, 'content', 'keyFileName');
+1 -1
View File
@@ -725,7 +725,7 @@
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p ng-show="record.name === 'MX' && domain.provider === 'namecheap'">{{ 'email.dnsStatus.namecheapInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/domains/#namecheap-dns" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/troubleshooting/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
<p ng-show="record.name === 'PTR'">{{ 'email.dnsStatus.ptrInfo' | tr }} <sup><a ng-href="https://docs.cloudron.io/email/#ptr-record" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
<p ng-show="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.hostname' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].name }}</tt></b></p>
<p ng-hide="expectedDnsRecords[record.value].name">{{ 'email.dnsStatus.domain' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
<p>{{ 'email.dnsStatus.type' | tr }}: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
+7 -1
View File
@@ -64,6 +64,12 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
Client.openSubscriptionSetup($scope.$parent.subscription);
};
function updateMailUsage(mailboxName, quotaLimit) {
if (!$scope.mailUsage) $scope.mailUsage = {};
if (!$scope.mailUsage[mailboxName]) $scope.mailUsage[mailboxName] = {};
$scope.mailUsage[mailboxName].quotaLimit = quotaLimit;
}
function refreshMailUsage() {
Client.getMailUsage($scope.domain.domain, function (error, usage) {
if (error) console.error(error);
@@ -646,7 +652,7 @@ angular.module('Application').controller('EmailController', ['$scope', '$locatio
}
function done() {
$scope.mailUsage[$scope.mailboxes.edit.name + '@' + $scope.domain.domain].quotaLimit = $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0; // hack to avoid refresh
updateMailUsage($scope.mailboxes.edit.name + '@' + $scope.domain.domain, $scope.mailboxes.edit.storageQuotaEnabled ? $scope.mailboxes.edit.storageQuota : 0); // hack to avoid refresh
$scope.mailboxes.edit.busy = false;
$scope.mailboxes.edit.error = null;
+1 -1
View File
@@ -7,7 +7,7 @@
<h1>
{{ 'emails.eventlog.title' | tr }}
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
<a class="btn btn-default btn-outline pull-right" href="/frontend/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
<a class="btn btn-default btn-outline pull-right" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
</h1>
</div>
+1 -1
View File
@@ -7,7 +7,7 @@
<h1>
{{ 'emails.queue.title' | tr }}
<a class="btn btn-default btn-outline pull-right" href="/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
<a class="btn btn-default btn-outline pull-right" href="/frontend/logs.html?id=mail" target="_blank">{{ 'main.action.logs' | tr }}</a>
</h1>
</div>
</div>
+73 -73
View File
@@ -1,50 +1,3 @@
<!-- Modal change mail server domain -->
<div class="modal fade" id="mailLocationModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'emails.changeDomainDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<div ng-bind-html=" 'emails.changeDomainDialog.description' | tr "></div>
<br>
<form name="mailLocationForm" role="form" novalidate ng-submit="mailLocation.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (mailLocationForm.subdomain.$dirty && mailLocationForm.subdomain.$invalid) || (!mailLocationForm.subdomain.$dirty && mailLocation.error)}">
<label class="control-label">{{ 'emails.changeDomainDialog.location' | tr }}</label>
<div class="has-error" ng-show="mailLocation.error">{{ mailLocation.error.message }}</div>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
<input class="ng-hide" type="submit" ng-disabled="mailLocationForm.$invalid"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="mailLocation.submit()" ng-disabled="mailLocationForm.$invalid || mailLocation.busy"><i class="fa fa-circle-notch fa-spin" ng-show="mailLocation.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal change max email size -->
<div class="modal fade" id="maxEmailSizeChangeModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -216,12 +169,11 @@
<div class="pull-right">
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-queue">{{ 'emails.action.queue' | tr }}</a>
<a class="btn btn-default" ng-show="user.isAtLeastOwner" href="#/emails-eventlog">{{ 'eventlog.title' | tr }}</a>
<!-- hidden for now, until we see a purpose -->
<!-- <a class="btn btn-sm btn-default" ng-disabled="user.isAtLeastOwner" href="/filemanager.html?id=mail&type=mail" target="_blank" uib-tooltip="{{ 'app.filemanagerActionTooltip' | tr }}" tooltip-append-to-body="true" tooltip-placement="bottom"><i class="fas fa-folder"></i></a> -->
</div>
</h1>
</div>
<!-- domain listing -->
<div class="text-left">
<h3>{{ 'emails.domains.title' | tr }}</h3>
</div>
@@ -271,7 +223,8 @@
</div>
</div>
<div class="text-left" ng-show="user.isAtLeastOwner">
<!-- mailbox sharing -->
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
<h3>{{ 'emails.mailboxSharing.title' | tr }}</h3>
</div>
@@ -291,22 +244,82 @@
</div>
</div>
<div class="text-left" ng-show="user.isAtLeastOwner">
<!-- server location -->
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
<h3>
{{ 'emails.settings.location' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="mailLocation.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'main.action.showLogs' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in mailLocation.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card">
<div class="row">
<div class="col-md-7">
<p ng-bind-html="'emails.changeDomainDialog.description' | tr"></p>
</div>
<div class="col-md-5">
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="mailLocation.subdomain" id="mailLocationLocationInput" name="location" placeholder="{{ 'emails.changeDomainDialog.locationPlaceholder' | tr }}" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!mailLocation.subdomain ? '' : '.') + mailLocation.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="mailLocation.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="mailLocation.busy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
</div>
</div>
</div>
<p class="text-center text-warning text-bold" ng-show="mailLocation.domain.provider === 'manual'" ng-bind-html="'emails.changeDomainDialog.manualInfo' | tr:{ domain: mailLocation.domain.domain }"></p>
<div class="row">
<div class="col-md-6">
<p ng-show="mailLocation.busy">{{ mailLocation.message }}</p>
<p ng-hide="mailLocation.busy">
<div class="has-error" ng-show="!mailLocation.active">{{ mailLocation.errorMessage }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<!-- save is always enabled so that user can "redo" the task -->
<button class="btn btn-outline btn-primary" ng-click="mailLocation.change()" ng-hide="mailLocation.busy">{{ 'main.dialog.save' | tr }}</button>
<button class="btn btn-outline btn-danger" ng-click="mailLocation.stop()" ng-show="mailLocation.busy" style="margin-right: 10px">{{ 'main.dialog.cancel' | tr }}</button>
</div>
</div>
</div>
<!-- settings -->
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
<h3>{{ 'emails.settings.title' | tr }}</h3>
</div>
<div class="card" ng-show="user.isAtLeastOwner" style="margin-bottom: 15px;">
<p ng-bind-html=" 'emails.settings.info' | tr "></p>
<div class="row">
<div class="col-xs-6">
<span class="text-muted">{{ 'emails.settings.location' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ mailLocation.currentLocation.subdomain + (!mailLocation.currentLocation.subdomain ? '' : '.') + mailLocation.currentLocation.domain.domain }}
<a ng-hide="mailLocation.busy" href="" ng-click="mailLocation.show()"><i class="fa fa-edit text-small"></i></a> <!-- ng-disabled does not work for links -->
</span>
</div>
<div class="col-xs-6">
<span class="text-muted">{{ 'emails.settings.maxMailSize' | tr }}</span>
</div>
@@ -341,19 +354,6 @@
<a href="" ng-click="solrConfig.show()"><i class="fa fa-edit text-small"></i></a>
</div>
</div>
<div class="row" ng-show="mailLocation.busy">
<div class="col-md-12" style="margin-top: 10px;">
{{ 'emails.settings.changeDomainProgress' | tr }}
<div style="display: flex; margin: 4px 0;">
<div class="progress progress-striped active animateMe" style="flex-grow: 1;">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ mailLocation.percent }}%"></div>
</div>
<div ng-show="mailLocation.taskMinutesActive >= 2" class="text-danger hand" style="margin: 0 4px;" ng-click="mailLocation.stopTask()" uib-tooltip="Cancel Task"><i class="fas fa-times"></i></div>
</div>
<p>{{ mailLocation.message }}</p>
</div>
</div>
</div>
</div>
+35 -69
View File
@@ -10,39 +10,30 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
$scope.user = Client.getUserInfo();
$scope.domains = [];
// this is required because we need to rewrite the MAIL_SERVER_NAME env var
$scope.reconfigureEmailApps = function () {
var installedApps = Client.getInstalledApps();
for (var i = 0; i < installedApps.length; i++) {
if (!installedApps[i].manifest.addons.email) continue;
Client.repairApp(installedApps[i].id, { }, function (error) {
if (error) console.error(error);
});
}
};
$scope.mailLocation = {
busy: false,
error: null,
percent: 0,
message: '',
errorMessage: '',
currentLocation: { domain: null, subdomain: '' },
domain: null,
subdomain: '',
taskId: null,
percent: 0,
taskMinutesActive: 0,
message: '',
errorMessage: '',
reconfigure: false,
tasks: [],
stopTask: function () {
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, tasks) {
if (error) return console.error(error);
if (!task.id) return;
$scope.mailLocation.tasks = tasks.slice(0, 10);
if ($scope.mailLocation.tasks.length && $scope.mailLocation.tasks[0].active) $scope.mailLocation.updateStatus();
});
},
Client.stopTask(task.id, function (error) {
if (error) console.error(error);
});
stop: function () {
Client.stopTask($scope.mailLocation.tasks[0].id, function (error) {
if (error) console.error(error);
$scope.mailLocation.busy = false;
$scope.mailLocation.refreshTasks();
});
},
@@ -50,46 +41,26 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
Client.getMailLocation(function (error, location) {
if (error) return console.error('Failed to get max email location', error);
$scope.mailLocation.currentLocation.subdomain = location.subdomain;
$scope.mailLocation.currentLocation.domain = $scope.domains.find(function (d) { return location.domain === d.domain; });
$scope.mailLocation.currentLocation.subdomain = $scope.mailLocation.subdomain = location.subdomain;
$scope.mailLocation.currentLocation.domain = $scope.mailLocation.domain = $scope.domains.find(function (d) { return location.domain === d.domain; });
Client.getLatestTaskByType(TASK_TYPES.TASK_CHANGE_MAIL_LOCATION, function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.mailLocation.taskId = task.id;
$scope.mailLocation.reconfigure = task.active; // if task is active when this view reloaded, reconfigure email apps when task done
$scope.mailLocation.updateStatus();
});
$scope.mailLocation.refreshTasks();
});
},
show: function () {
$scope.mailLocation.busy = false;
$scope.mailLocation.error = null;
$scope.mailLocation.domain = $scope.mailLocation.currentLocation.domain;
$scope.mailLocation.subdomain = $scope.mailLocation.currentLocation.subdomain;
$scope.mailLocationForm.$setUntouched();
$scope.mailLocationForm.$setPristine();
$('#mailLocationModal').modal('show');
},
updateStatus: function () {
Client.getTask($scope.mailLocation.taskId, function (error, data) {
var taskId = $scope.mailLocation.tasks[0].id;
Client.getTask(taskId, function (error, data) {
if (error) return window.setTimeout($scope.mailLocation.updateStatus, 5000);
if (!data.active) {
$scope.mailLocation.taskId = null;
$scope.mailLocation.busy = false;
$scope.mailLocation.message = '';
$scope.mailLocation.percent = 0;
$scope.taskMinutesActive = 0;
$scope.mailLocation.percent = 100;
$scope.mailLocation.errorMessage = data.success ? '' : data.error.message;
if ($scope.mailLocation.reconfigure) $scope.reconfigureEmailApps();
$scope.mailLocation.refreshTasks(); // update the tasks list dropdown
return;
}
@@ -97,32 +68,26 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
$scope.mailLocation.busy = true;
$scope.mailLocation.percent = data.percent;
$scope.mailLocation.message = data.message;
$scope.mailLocation.taskMinutesActive = moment().diff(moment(data.creationTime), 'minutes');
window.setTimeout($scope.mailLocation.updateStatus, 1000);
});
},
submit: function () {
change: function () {
$scope.mailLocation.busy = true;
$scope.mailLocation.percent = 0;
$scope.mailLocation.message = '';
$scope.mailLocation.errorMessage = '';
Client.setMailLocation($scope.mailLocation.subdomain, $scope.mailLocation.domain.domain, function (error, result) {
if (error) {
console.error(error);
$scope.mailLocation.errorMessage = error.message;
$scope.mailLocation.busy = false;
$scope.mailLocation.error = error;
return;
} else {
$scope.mailLocation.refreshTasks();
}
// update UI immediately
$scope.mailLocation.currentLocation = { subdomain: $scope.mailLocation.subdomain, domain: $scope.mailLocation.domain };
$scope.mailLocation.taskId = result.taskId;
$scope.mailLocation.reconfigure = true; // reconfigure email apps when task done
$scope.mailLocation.updateStatus();
Client.refreshConfig(); // update config.mailFqdn
$('#mailLocationModal').modal('hide');
});
}
};
@@ -438,8 +403,9 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati
if (error) return console.error('Failed to fetch usage for domain', domain.domain, error);
domain.usage = 0;
// quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
Object.keys(usage).forEach(function (m) { domain.usage += (usage[m].quotaValue || usage[m].diskSize); });
// we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently
// also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; });
});
});
});
-2
View File
@@ -32,7 +32,6 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
{ name: 'app.start', value: 'app.start' },
{ name: 'app.stop', value: 'app.stop' },
{ name: 'app.restart', value: 'app.restart' },
{ name: 'Apptask Crash', value: 'app.task.crash' },
{ name: 'backup.cleanup', value: 'backup.cleanup.start' },
{ name: 'backup.cleanup.finish', value: 'backup.cleanup.finish' },
{ name: 'backup.finish', value: 'backup.finish' },
@@ -74,7 +73,6 @@ angular.module('Application').controller('EventLogController', ['$scope', '$loca
{ name: 'volume.add', value: 'volume.add' },
{ name: 'volume.update', value: 'volume.update' },
{ name: 'volume.remove', value: 'volume.update' },
{ name: 'System Crash', value: 'system.crash' }
];
$scope.pageItemCount = [
+97 -47
View File
@@ -71,6 +71,32 @@
</div>
</div>
<!-- Modal Trusted IPs -->
<div class="modal fade" id="trustedIpsModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'network.trustedIps.title' | tr }}</h4>
</div>
<div class="modal-body">
<form name="trustedIpsChangeForm" role="form" novalidate ng-submit="trustedIps.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label">{{ 'network.trustedIpRanges' | tr }}</label>
<p class="small">{{ 'network.trustedIps.description' | tr }}</p>
<div class="has-error" ng-show="trustedIps.error.trustedIps">{{ trustedIps.error.trustedIps }}</div>
<textarea ng-model="trustedIps.trustedIps" placeholder="{{ 'network.firewall.configure.blocklistPlaceholder' | tr }}" name="trustedIps" class="form-control" ng-class="{ 'has-error': !trustedIpsChangeForm.trustedIps.$dirty && trustedIps.error.trustedIps }" rows="4"></textarea>
</div>
<input class="ng-hide" type="submit"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="trustedIps.submit()"><i class="fa fa-circle-notch fa-spin" ng-show="trustedIps.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal IPv6 -->
<div class="modal fade" id="ipv6ConfigureModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -173,8 +199,59 @@
</div>
</div>
<!-- IPv6 -->
<div class="text-left section-header">
<h3>{{ 'network.ipv6.title' | tr }}</h3>
</div>
<div class="card">
<div class="row">
<div class="col-xs-12">
{{ 'network.ipv6.description' | tr }}
</div>
</div>
<br/>
<div class="row">
<div class="col-xs-2">
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
</div>
<div class="col-xs-10 text-right">
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
</div>
</div>
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
<div class="col-xs-2">
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
</div>
<div class="col-xs-10 text-right">
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
</div>
</div>
<div class="row" ng-show="ipv6Configure.ifname">
<div class="col-xs-6">
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ ipv6Configure.ifname }}</span>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-6 col-md-offset-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
</div>
</div>
</div>
<!-- Firewall -->
<div class="text-left" ng-show="user.isAtLeastOwner">
<div class="text-left section-header" ng-show="user.isAtLeastOwner">
<h3>{{ 'network.firewall.title' | tr }}</h3>
</div>
@@ -187,61 +264,34 @@
<span>{{ 'network.firewall.blocklist' | tr:{ blockCount: blocklist.currentBlocklistLength } }} <a href="" ng-click="blocklist.show()"><i class="fa fa-edit text-small"></i></a></span>
</div>
</div>
</div>
<!-- IPv6 -->
<div class="text-left">
<h3>{{ 'network.ipv6.title' | tr }}</h3>
</div>
<div class="card">
<div class="row">
<div class="col-xs-12">
{{ 'network.ipv6.description' | tr }}
</div>
</div>
<br/>
<div class="row">
<div class="col-xs-2">
<span class="text-muted">{{ 'network.ip.provider' | tr }}</span>
</div>
<div class="col-xs-10 text-right">
<span>{{ prettyIpProviderName(ipv6Configure.provider) }}</span>
</div>
</div>
<div class="row" ng-show="ipv6Configure.provider !== 'noop'">
<div class="col-xs-2">
<span class="text-muted">{{ 'network.ip.address' | tr }}</span>
</div>
<div class="col-xs-10 text-right">
<span ng-show="ipv6Configure.ipv6">{{ ipv6Configure.ipv6 }}</span>
<span ng-show="!ipv6Configure.ipv6 && ipv6Configure.serverIPv6">{{ ipv6Configure.serverIPv6 }} ({{ 'network.ip.detected' | tr }})</span>
<span ng-show="ipv6Configure.displayError" class="text-danger">{{ ipv6Configure.displayError }}</span>
</div>
</div>
<div class="row" ng-show="ipv6Configure.ifname">
<div class="col-xs-6">
<span class="text-muted">{{ 'network.ip.interface' | tr }}</span>
<span class="text-muted">{{ 'network.trustedIpRanges' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ ipv6Configure.ifname }}</span>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-6 col-md-offset-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="ipv6Configure.show()">{{ 'network.ip.configure' | tr }}</button>
<span>{{ 'network.trustedIps.summary' | tr:{ trustCount: trustedIps.currentTrustedIpsLength } }} <a href="" ng-click="trustedIps.show()"><i class="fa fa-edit text-small"></i></a></span>
</div>
</div>
</div>
<div class="text-left">
<h3>{{ 'network.dyndns.title' | tr }}</h3>
<!-- Dynamic DNS -->
<div class="text-left section-header">
<h3>
{{ 'network.dyndns.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="dyndnsConfigure.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'network.dyndns.showLogsAction' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in dyndnsConfigure.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card">
+62 -4
View File
@@ -1,9 +1,9 @@
'use strict';
/* global angular */
/* global $ */
/* global $, TASK_TYPES */
angular.module('Application').controller('NetworkController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
angular.module('Application').controller('NetworkController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
$scope.user = Client.getUserInfo();
@@ -37,12 +37,25 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
busy: false,
error: '',
isEnabled: false,
tasks: [],
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_SYNC_DYNDNS, function (error, tasks) {
if (error) return console.error(error);
$scope.dyndnsConfigure.tasks = tasks.slice(0, 10);
if ($scope.dyndnsConfigure.tasks.length && $scope.dyndnsConfigure.tasks[0].active) {
$timeout($scope.renewCerts.refreshTasks, 5000);
}
});
},
refresh: function () {
Client.getDynamicDnsConfig(function (error, enabled) {
if (error) return console.error(error);
$scope.dyndnsConfigure.isEnabled = enabled;
$scope.dyndnsConfigure.refreshTasks();
});
},
@@ -192,6 +205,50 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
}
};
$scope.trustedIps = {
busy: false,
error: {},
trustedIps: '',
currentTrustedIps: '',
currentTrustedIpsLength: 0,
refresh: function () {
Client.getTrustedIps(function (error, result) {
if (error) return console.error(error);
$scope.trustedIps.currentTrustedIps = result;
$scope.trustedIps.currentTrustedIpsLength = result.split('\n').filter(function (l) { return l.length !== 0 && l[0] !== '#'; }).length;
});
},
show: function () {
$scope.trustedIps.error = {};
$scope.trustedIps.trustedIps = $scope.trustedIps.currentTrustedIps;
$('#trustedIpsModal').modal('show');
},
submit: function () {
$scope.trustedIps.error = {};
$scope.trustedIps.busy = true;
Client.setTrustedIps($scope.trustedIps.trustedIps, function (error) {
$scope.trustedIps.busy = false;
if (error) {
$scope.trustedIps.error.trustedIps = error.message;
$scope.trustedIps.error.ip = error.message;
$scope.trustedIpsChangeForm.$setPristine();
$scope.trustedIpsChangeForm.$setUntouched();
return;
}
$scope.trustedIps.refresh();
$('#trustedIpsModal').modal('hide');
});
}
};
$scope.sysinfo = {
busy: false,
error: {},
@@ -208,7 +265,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
newIfname: '',
refresh: function () {
Client.getSysinfoConfig(function (error, result) {
Client.getIPv4Config(function (error, result) {
if (error) return console.error(error);
$scope.sysinfo.provider = result.provider;
@@ -246,7 +303,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
config.ifname = $scope.sysinfo.newIfname;
}
Client.setSysinfoConfig(config, function (error) {
Client.setIPv4Config(config, function (error) {
$scope.sysinfo.busy = false;
if (error && error.message.indexOf('ipv') !== -1) {
$scope.sysinfo.error.ipv4 = error.message;
@@ -276,6 +333,7 @@ angular.module('Application').controller('NetworkController', ['$scope', '$locat
$scope.dyndnsConfigure.refresh();
$scope.ipv6Configure.refresh();
$scope.trustedIps.refresh();
if ($scope.user.isAtLeastOwner) $scope.blocklist.refresh();
});
-209
View File
@@ -1,209 +0,0 @@
<!-- Modal client add -->
<div class="modal fade" id="clientAddModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.newClientDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
{{ 'oidc.newClientDialog.description' | tr }}
<br/>
<br/>
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" for="clientName">{{ 'oidc.client.name' | tr }}</label>
<input type="text" id="clientName" class="form-control" name="clientName" ng-model="clientAdd.name" autofocus required/>
</div>
<div class="form-group" ng-class="{ 'has-error': clientAdd.error.id }">
<label class="control-label" for="clientId">{{ 'oidc.client.id' | tr }}</label>
<input type="text" id="clientId" class="form-control" name="clientId" ng-model="clientAdd.id" required/>
<div class="control-label" ng-show="clientAdd.error.id">
<small>{{ clientAdd.error.id }}</small>
</div>
</div>
<div class="form-group">
<label class="control-label" for="clientSecret">{{ 'oidc.client.secret' | tr }}</label>
<input type="text" id="clientSecret" class="form-control" name="clientSecret" ng-model="clientAdd.secret" required/>
</div>
<div class="form-group">
<label class="control-label" for="loginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
<input type="text" id="loginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientAdd.loginRedirectUri" required/>
</div>
<div class="form-group">
<label class="control-label" for="logoutRedirectUri">{{ 'oidc.client.logoutRedirectUri' | tr }}</label>
<input type="url" id="logoutRedirectUri" class="form-control" name="logoutRedirectUri" ng-model="clientAdd.logoutRedirectUri"/>
</div>
<div class="form-group">
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
<div class="control-label">
<select class="form-control" ng-model="clientAdd.tokenSignatureAlgorithm">
<option value="RS256">RS256</option>
<option value="EdDSA">EdDSA</option>
</select>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="clientAddForm.$invalid"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="clientAdd.busy"></i> {{ 'oidc.newClientDialog.createAction' | tr }}
</button>
</div>
</div>
</div>
</div>
<!-- Modal client edit -->
<div class="modal fade" id="clientEditModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.id } }}</h4>
</div>
<div class="modal-body">
<form name="clientEditForm" role="form" novalidate ng-submit="clientEdit.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" for="inputEditClientName">{{ 'oidc.client.name' | tr }}</label>
<input type="text" id="inputEditClientName" class="form-control" name="clientName" ng-model="clientEdit.name" autofocus required/>
</div>
<div class="form-group">
<label class="control-label" for="inputEditClientSecret">{{ 'oidc.client.secret' | tr }}</label>
<input type="text" id="inputEditClientSecret" class="form-control" name="clientSecret" ng-model="clientEdit.secret" required/>
</div>
<div class="form-group">
<label class="control-label" for="inputEditLoginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
<input type="text" id="inputEditLoginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientEdit.loginRedirectUri" required/>
</div>
<div class="form-group">
<label class="control-label" for="inputEditLogoutRedirectUri">{{ 'oidc.client.logoutRedirectUri' | tr }}</label>
<input type="url" id="inputEditLogoutRedirectUri" class="form-control" name="logoutRedirectUri" ng-model="clientEdit.logoutRedirectUri"/>
</div>
<div class="form-group">
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
<div class="control-label">
<select class="form-control" ng-model="clientEdit.tokenSignatureAlgorithm">
<option value="RS256">RS256</option>
<option value="EdDSA">EdDSA</option>
</select>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="clientEditForm.$invalid"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="clientEdit.submit()" ng-disabled="clientEditForm.$invalid || clientEdit.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="clientEdit.busy"></i> {{ 'main.dialog.save' | tr }}
</button>
</div>
</div>
</div>
</div>
<!-- Modal client delete -->
<div class="modal fade" id="clientDeleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.deleteClientDialog.title' | tr:{ client: deleteClient.id } }}</h4>
</div>
<div class="modal-body">
<p>{{ 'oidc.deleteClientDialog.description' | tr }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-danger" ng-click="deleteClient.submit()" ng-disabled="deleteClient.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteClient.busy"></i> {{ 'main.dialog.delete' | tr }}</button>
</div>
</div>
</div>
</div>
<div class="content">
<div class="text-left">
<h1>{{ 'oidc.title' | tr }}</h1>
</div>
<div class="card">
<div class="grid-item-top">
<div class="row">
<div class="col-md-12">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/.well-known/openid-configuration</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.authEndpoint' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/auth</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.tokenEndpoint' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/token</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.keysEndpoint' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/jwks</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.profileEndpoint' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/me</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.logoutUrl' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/openid/session/end</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<br>
<div class="text-left">
<h3>{{ 'oidc.clients.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> {{ 'oidc.clients.newClient' | tr }}</button></h3>
</div>
<div class="card">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 33%">{{ 'oidc.client.name' | tr }}</th>
<th style="width: 33%">{{ 'oidc.client.id' | tr }}</th>
<th style="width: 33%">{{ 'oidc.client.signingAlgorithm' | tr }}</th>
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
</tr>
</thead>
<tbody>
<tr ng-show="clients.length === 0">
<td colspan="3" class="text-center">{{ 'oidc.clients.empty' | tr }}</td>
</tr>
<tr ng-repeat="client in clients">
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.name }}
</td>
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.id }}
</td>
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.tokenSignatureAlgorithm }}
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-danger" ng-click="deleteClient.show(client)" uib-tooltip="Delete"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="clientEdit.show(client)" uib-tooltip="Edit"><i class="far fa fa-pencil-alt"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
-153
View File
@@ -1,153 +0,0 @@
'use strict';
/* global angular */
/* global $ */
angular.module('Application').controller('OidcController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.clients = [];
$scope.refreshClients = function () {
Client.getOidcClients(function (error, result) {
if (error) return console.error('Failed to load oidc clients', error);
$scope.clients = result;
});
};
$scope.clientAdd = {
busy: false,
error: {},
id: '',
name: '',
secret: '',
loginRedirectUri: '',
logoutRedirectUri: '',
tokenSignatureAlgorithm: '',
show: function () {
$scope.clientAdd.id = '';
$scope.clientAdd.secret = '';
$scope.clientAdd.name = '';
$scope.clientAdd.loginRedirectUri = '';
$scope.clientAdd.logoutRedirectUri = '';
$scope.clientAdd.tokenSignatureAlgorithm = 'RS256';
$scope.clientAdd.busy = false;
$scope.clientAdd.error = null;
$scope.clientAddForm.$setPristine();
$('#clientAddModal').modal('show');
},
submit: function () {
$scope.clientAdd.busy = true;
$scope.clientAdd.error = {};
Client.addOidcClient($scope.clientAdd.id, $scope.clientAdd.name, $scope.clientAdd.secret, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.logoutRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.clientAdd.error.id = 'Client ID already exists';
$('#clientId').focus();
} else {
console.error('Unable to add openid client.', error);
}
$scope.clientAdd.busy = false;
return;
}
$scope.refreshClients();
$scope.clientAdd.busy = false;
$('#clientAddModal').modal('hide');
});
}
};
$scope.clientEdit = {
busy: false,
error: {},
id: '',
name: '',
secret: '',
loginRedirectUri: '',
logoutRedirectUri: '',
tokenSignatureAlgorithm: '',
show: function (client) {
$scope.clientEdit.id = client.id;
$scope.clientEdit.name = client.name;
$scope.clientEdit.secret = client.secret;
$scope.clientEdit.loginRedirectUri = client.loginRedirectUri;
$scope.clientEdit.logoutRedirectUri = client.logoutRedirectUri;
$scope.clientEdit.tokenSignatureAlgorithm = client.tokenSignatureAlgorithm;
$scope.clientEdit.busy = false;
$scope.clientEdit.error = null;
$scope.clientEditForm.$setPristine();
$('#clientEditModal').modal('show');
},
submit: function () {
$scope.clientEdit.busy = true;
$scope.clientEdit.error = {};
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.secret, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.logoutRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
if (error) {
console.error('Unable to edit openid client.', error);
$scope.clientEdit.busy = false;
return;
}
$scope.refreshClients();
$scope.clientEdit.busy = false;
$('#clientEditModal').modal('hide');
});
}
};
$scope.deleteClient = {
busy: false,
error: {},
id: '',
show: function (client) {
$scope.deleteClient.busy = false;
$scope.deleteClient.id = client.id;
$('#clientDeleteModal').modal('show');
},
submit: function () {
Client.delOidcClient($scope.deleteClient.id, function (error) {
$scope.deleteClient.busy = false;
if (error) return console.error('Failed to delete openid client', error);
$scope.refreshClients();
$('#clientDeleteModal').modal('hide');
});
}
};
Client.onReady(function () {
$scope.refreshClients();
});
// setup all the dialog focus handling
['clientAddModal', 'clientEditmodal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
$('.modal-backdrop').remove();
}]);
+2 -2
View File
@@ -541,8 +541,8 @@
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<p>{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: cliTokens.length } }}</p>
<button class="btn btn-outline btn-danger pull-right" ng-click="logoutFromAll()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> {{ 'profile.loginTokens.logoutAll' | tr }}</button>
<p>{{ 'profile.loginTokens.description' | tr:{ webadminTokenCount: tokens.webadminTokens.length, cliTokenCount: tokens.cliTokens.length } }}</p>
<button class="btn btn-outline btn-danger pull-right" ng-click="tokens.revokeAllWebAndCliTokens()" ng-disabled="tokens.busy"><i class="fa fa-circle-notch fa-spin" ng-show="tokens.busy"></i> {{ 'profile.loginTokens.logoutAll' | tr }}</button>
</div>
</div>
</div>
+5 -11
View File
@@ -3,6 +3,7 @@
/* global async, Clipboard */
/* global angular */
/* global $ */
/* global TOKEN_TYPES */
angular.module('Application').controller('ProfileController', ['$scope', '$translate', '$location', 'Client', '$timeout', function ($scope, $translate, $location, Client, $timeout) {
$scope.user = Client.getUserInfo();
@@ -636,9 +637,10 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
$scope.tokens.busy = false;
$scope.tokens.allTokens = result;
$scope.tokens.webadminTokens = result.filter(function (c) { return c.clientId === 'cid-webadmin'; });
$scope.tokens.cliTokens = result.filter(function (c) { return c.clientId === 'cid-cli'; });
$scope.tokens.apiTokens = result.filter(function (c) { return c.clientId === 'cid-sdk'; });
// dashboard and development clientIds were issued with 7.5.0
$scope.tokens.webadminTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; });
$scope.tokens.cliTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; });
$scope.tokens.apiTokens = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_SDK; });
});
},
@@ -709,14 +711,6 @@ angular.module('Application').controller('ProfileController', ['$scope', '$trans
}
};
$scope.logoutFromAll = function () {
Client.destroyOidcSession(function (error) {
if (error) console.error('Failed to destroy oidc session', error);
$scope.tokens.revokeAllWebAndCliTokens();
});
};
Client.onReady(function () {
$scope.appPassword.refresh();
$scope.tokens.refresh();
+4 -4
View File
@@ -81,7 +81,7 @@
<td class="elide-table-cell"></td>
<td class="elide-table-cell text-center"></td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<a class="btn btn-xs btn-default" href="/logs.html?id=box" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
<a class="btn btn-xs btn-default" href="/frontend/logs.html?id=box" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
</td>
</tr>
<tr ng-repeat="service in services | filter:{ isRedis: false } | orderBy:'name'">
@@ -105,7 +105,7 @@
</td>
<td class="elide-table-cell">
<div class="progress progress-striped" ng-show="service.config.memoryLimit">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%"></div>
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ service.memoryPercent }}%">{{ service.memoryPercent }}%</div>
</div>
</td>
<td class="elide-table-cell text-center">
@@ -114,7 +114,7 @@
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-disabled="!service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' && !service.config.recoveryMode }"></i></button>
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
</td>
</tr>
<tr ng-show="hasRedisServices" ng-click="redisServicesExpanded = !redisServicesExpanded" class="hand">
@@ -142,7 +142,7 @@
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="serviceConfigure.show(service)" uib-tooltip="{{ 'services.configureActionTooltip' | tr }}" ng-show="service.config.memoryLimit"><i class="fa fa-pencil-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="restartService(service.name)" uib-tooltip="{{ 'services.restartActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': service.status === 'starting' && !service.config.recoveryMode }"></i></button>
<a class="btn btn-xs btn-default" ng-href="{{ '/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/logs.html?id=' + service.name }}" target="_blank" uib-tooltip="{{ 'logs.title' | tr }}"><i class="fa fa-file-alt"></i></a>
</td>
</tr>
</tbody>
+19 -6
View File
@@ -204,7 +204,7 @@
</div>
</div>
<div class="text-left">
<div class="text-left section-header">
<h3>{{ 'settings.timezone.title' | tr }}</h3>
</div>
@@ -228,7 +228,7 @@
</div>
</div>
<div class="text-left">
<div class="text-left section-header">
<h3>{{ 'settings.language.title' | tr }}</h3>
</div>
@@ -251,8 +251,22 @@
</div>
</div>
<div class="text-left">
<h3>{{ 'settings.updates.title' | tr }}</h3>
<div class="text-left section-header">
<h3>
{{ 'settings.updates.title' | tr }}
<div class="btn-group btn-group-sm pull-right">
<button type="button" class="btn btn-small btn-default dropdown-toggle" ng-show="update.tasks.length" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" uib-tooltip="{{ 'settings.updates.showLogsAction' | tr }}">
<i class="fas fa-align-left"></i> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="task in update.tasks">
<a ng-href="/frontend/logs.html?taskId={{task.id}}" target="_blank" class="text-right">
{{ task.ts | prettyLongDate }} <i class="fa" style="margin-left: 20px" ng-class="{ 'status-active fa-check-circle': !task.active && task.success, 'fa-circle-notch fa-spin': task.active, 'status-error fa-times-circle': !task.active && !task.success }"></i>
</a>
</li>
</ul>
</div>
</h3>
</div>
<div class="card" style="margin-bottom: 15px;">
@@ -286,7 +300,6 @@
<div class="row" ng-show="update.busy">
<div class="col-md-12">
<p >{{ update.message }}</p>
<p class="has-error" ng-show="update.errorMessage">{{ update.errorMessage }}. <a ng-class="warning" ng-href="/logs.html?taskId={{update.taskId}}" target="_blank">{{ 'settings.updates.showLogsAction' | tr }}</a></p>
</div>
</div>
@@ -300,7 +313,7 @@
</div>
</div>
<div class="text-left">
<div class="text-left section-header">
<h3>{{ 'settings.privateDockerRegistry.title' | tr }}</h3>
</div>
+22 -19
View File
@@ -1,7 +1,7 @@
'use strict';
/* global angular:false */
/* global $:false */
/* global $:false, TASK_TYPES */
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$translate', '$rootScope', '$timeout', 'Client', function ($scope, $location, $translate, $rootScope, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
@@ -87,8 +87,16 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
percent: 0,
message: 'Downloading',
errorMessage: '', // this shows inline
taskId: '',
skipBackup: false,
tasks: [],
refreshTasks: function () {
Client.getTasksByType(TASK_TYPES.TASK_UPDATE, function (error, tasks) {
if (error) return console.error(error);
$scope.update.tasks = tasks.slice(0, 10);
if ($scope.update.tasks.length && $scope.update.tasks[0].active) $scope.update.updateStatus();
});
},
checkNow: function () {
$scope.update.checking = true;
@@ -108,7 +116,9 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
},
stopUpdate: function () {
Client.stopTask($scope.update.taskId, function (error) {
var taskId = $scope.update.tasks[0].id;
Client.stopTask(taskId, function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.update.errorMessage = 'No update is currently in progress';
@@ -124,18 +134,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
},
checkStatus: function () {
Client.getLatestTaskByType('update', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.update.taskId = task.id;
$scope.update.updateStatus();
});
},
reloadIfNeeded: function () {
Client.getStatus(function (error, status) {
Client.getProvisionStatus(function (error, status) {
if (error) return $scope.error(error);
if (window.localStorage.version !== status.version) window.location.reload(true);
@@ -143,7 +143,9 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
},
updateStatus: function () {
Client.getTask($scope.update.taskId, function (error, data) {
var taskId = $scope.update.tasks[0].id;
Client.getTask(taskId, function (error, data) {
if (error) return window.setTimeout($scope.update.updateStatus, 5000);
if (!data.active) {
@@ -154,6 +156,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
if (!data.errorMessage) $scope.update.reloadIfNeeded(); // assume success
$scope.update.refreshTasks(); // redundant... update the tasks list dropdown
return;
}
@@ -172,7 +176,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.update.message = '';
$scope.update.errorMessage = '';
Client.update({ skipBackup: $scope.update.skipBackup }, function (error, taskId) {
Client.update({ skipBackup: $scope.update.skipBackup }, function (error /*, taskId */) {
if (error) {
$scope.update.error.generic = error.message;
$scope.update.busy = false;
@@ -181,8 +185,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$('#updateModal').modal('hide');
$scope.update.taskId = taskId;
$scope.update.updateStatus();
$scope.update.refreshTasks();
});
}
};
@@ -430,7 +433,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
});
$scope.update.checkStatus();
$scope.update.refreshTasks();
if ($scope.user.isAtLeastOwner) getSubscription();
});
+1 -1
View File
@@ -69,7 +69,7 @@
</div>
</div>
<div class="text-left">
<div class="text-left section-header">
<h3>{{ 'support.remoteSupport.title' | tr }}</h3>
</div>
+4 -2
View File
@@ -4,7 +4,7 @@
<div class="col-md-12">
<h1>
{{ 'system.title' | tr }}
<a class="btn btn-default pull-right" href="/logs.html?id=box" target="_blank">{{ 'main.action.logs' | tr }}</a>
<a class="btn btn-default pull-right" href="/frontend/logs.html?id=box" target="_blank">{{ 'main.action.logs' | tr }}</a>
<button class="btn btn-default pull-right" ng-click="$parent.reboot.show()">{{ 'main.action.reboot' | tr }}</button>
</h1>
</div>
@@ -68,6 +68,7 @@
</div>
<div ng-hide="disks.busy" class="ng-hide">
<div class="row" ng-repeat="disk in disks.disks" style="margin-bottom: 20px;">
<hr style="margin: 5px 0px;" ng-show="$index !== 0"/>
<div class="col-md-12">
<div style="display: flex; align-items: baseline; justify-content: space-between;">
<h3 class="no-wrap" style="font-size: 20px;" ng-bind-html="'system.diskUsage.mountedAt' | tr:{ filesystem: disk.filesystem, mountpoint: disk.mountpoint }"></h3>
@@ -78,11 +79,12 @@
<div class="progress-bar" ng-repeat="content in disk.contents" style="width: {{ content.usage / disk.size * 100 }}%; background-color: {{ content.color }};" uib-tooltip="{{ content.label + ' ' + (content.usage | prettyDiskSize) }}"></div>
<div class="text-center text-muted" style="font-size: 12px; line-height: 20px;">{{ disk.available | prettyDiskSize }}</div>
</div>
<div class="text-right text-muted" style="margin-top: 10px;">{{ 'system.diskUsage.diskSpeed' | tr:{ speed: disk.speed } }}</div>
<div class="text-right text-muted" style="margin-top: 10px;" ng-show="disk.speed !== -1">{{ 'system.diskUsage.diskSpeed' | tr:{ speed: disk.speed } }}</div>
<p ng-hide="disk.volume">{{ 'system.diskUsage.diskContent' | tr }}:</p>
<p ng-show="disk.volume" ng-bind-html="'system.diskUsage.volumeContent' | tr:{ name: disk.volume.name }"></p>
<div ng-repeat="content in disk.contents" class="disk-content">
<span class="color-indicator" style="background-color: {{ content.color }};">&nbsp;</span>
<span ng-show="content.type === 'cloudron-backup-default'">{{ content.path }} (Old Backups)</span>
<span ng-show="content.type === 'standard'">{{ content.label || content.id }}</span>
<span ng-show="content.type === 'swap'">{{ content.id }}</span>
<span ng-show="content.type === 'app'">
+5 -1
View File
@@ -85,14 +85,18 @@ angular.module('Application').controller('SystemController', ['$scope', '$locati
disk.contents.forEach(function (content) { if (content.path === disk.mountpoint) disk.volume = $scope.volumesById[content.id]; });
disk.contents = disk.contents.filter(function (content) { return content.path !== disk.mountpoint; });
// only show old backups if the size is significant
disk.contents = disk.contents.filter(function (content) { return content.id !== 'cloudron-backup-default' || content.usage > 1024*1024*1024; });
disk.contents.forEach(function (content) {
content.color = getNextColor();
if (content.type === 'app') {
content.app = Client.getInstalledAppsByAppId()[content.id];
if (!content.app) content.uninstalled = true;
} else if (content.type === 'volume') {
content.volume = $scope.volumesById[content.id];
}
if (content.type === 'volume') content.volume = $scope.volumesById[content.id];
usageOther -= content.usage;
});
+531
View File
@@ -0,0 +1,531 @@
<!-- Modal external ldap -->
<div class="modal fade" id="externalLdapModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'users.externalLdapDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<p class="has-error text-center" ng-show="externalLdap.error.generic">{{ externalLdap.error.generic }}</p>
<div class="form-group">
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" id="ldapProvider" ng-model="externalLdap.provider" ng-options="a.value as a.name for a in ldapProvider"></select>
</div>
<div uib-collapse="externalLdap.provider === 'noop'">
<form name="externalLdapConfigForm" role="form" novalidate ng-submit="externalLdap.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.url }">
<label class="control-label" for="inputExternalLdapConfigUrl">{{ 'users.externalLdap.server' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.url" id="inputExternalLdapConfigUrl" name="url" ng-disabled="externalLdap.busy" placeholder="ldaps://example.com:636" required>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.acceptSelfSignedCerts"> {{ 'users.externalLdap.acceptSelfSignedCert' | tr }}
</label>
</div>
<p class="has-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.baseDn }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigBaseDn">{{ 'users.externalLdap.baseDn' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.baseDn" id="inputExternalLdapConfigBaseDn" name="baseDn" ng-disabled="externalLdap.busy" placeholder="ou=users,dc=example,dc=com" ng-required="externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.filter }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigFilter">{{ 'users.externalLdap.filter' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.filter" id="inputExternalLdapConfigFilter" name="filter" ng-disabled="externalLdap.busy" placeholder="(objectClass=inetOrgPerson)" ng-required="externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.usernameField }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigUsernameField">{{ 'users.externalLdap.usernameField' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.usernameField" id="inputExternalLdapConfigUsernameField" name="usernameField" ng-disabled="externalLdap.busy" placeholder="uid or sAMAcountName">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.syncGroups"> {{ 'users.externalLdap.syncGroups' | tr }}</sup>
</label>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupBaseDn }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupBaseDn">{{ 'users.externalLdap.groupBaseDn' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupBaseDn" id="inputExternalLdapConfigGroupBaseDn" name="groupBaseDn" ng-disabled="externalLdap.busy" placeholder="ou=groups,dc=example,dc=com" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupFilter }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupFilter">{{ 'users.externalLdap.groupFilter' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupFilter" id="inputExternalLdapConfigGroupFilter" name="groupFilter" ng-disabled="externalLdap.busy" placeholder="(objectClass=groupOfNames)" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupnameField }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupnameField">{{ 'users.externalLdap.groupnameField' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupnameField" id="inputExternalLdapConfigGroupnameField" name="groupnameField" ng-disabled="externalLdap.busy" placeholder="cn" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigBindDn">{{ 'users.externalLdap.bindUsername' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.bindDn" id="inputExternalLdapConfigBindDn" name="bindDn" ng-disabled="externalLdap.busy" placeholder="uid=admin,ou=Users,dc=example,dc=com">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }">
<label class="control-label" for="inputExternalLdapConfigBindPassword">{{ 'users.externalLdap.bindPassword' | tr }}</label>
<input type="password" class="form-control" ng-model="externalLdap.bindPassword" id="inputExternalLdapConfigBindPassword" name="bindPassword" ng-disabled="externalLdap.busy" placeholder="" password-reveal>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.autoCreate"> {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}
</label>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="externalLdapConfigForm.$invalid"/>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.busy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal client add -->
<div class="modal fade" id="oidcClientAddModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.newClientDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
{{ 'oidc.newClientDialog.description' | tr }}
<br/>
<br/>
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" for="clientName">{{ 'oidc.client.name' | tr }}</label>
<input type="text" id="clientName" class="form-control" name="clientName" ng-model="clientAdd.name" autofocus required/>
</div>
<div class="form-group" ng-class="{ 'has-error': clientAdd.error.id }">
<label class="control-label" for="clientId">{{ 'oidc.client.id' | tr }}</label>
<input type="text" id="clientId" class="form-control" name="clientId" ng-model="clientAdd.id" required/>
<div class="control-label" ng-show="clientAdd.error.id">
<small>{{ clientAdd.error.id }}</small>
</div>
</div>
<div class="form-group">
<label class="control-label" for="clientSecret">{{ 'oidc.client.secret' | tr }}</label>
<input type="text" id="clientSecret" class="form-control" name="clientSecret" ng-model="clientAdd.secret" required/>
</div>
<div class="form-group">
<label class="control-label" for="loginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
<input type="text" id="loginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientAdd.loginRedirectUri" required/>
</div>
<div class="form-group">
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
<div class="control-label">
<select class="form-control" ng-model="clientAdd.tokenSignatureAlgorithm">
<option value="RS256">RS256</option>
<option value="EdDSA">EdDSA</option>
</select>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="clientAddForm.$invalid"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="clientAdd.busy"></i> {{ 'oidc.newClientDialog.createAction' | tr }}
</button>
</div>
</div>
</div>
</div>
<!-- Modal client edit -->
<div class="modal fade" id="oidcClientEditModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.editClientDialog.title' | tr:{ client: clientEdit.id } }}</h4>
</div>
<div class="modal-body">
<form name="clientEditForm" role="form" novalidate ng-submit="clientEdit.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" for="inputEditClientName">{{ 'oidc.client.name' | tr }}</label>
<input type="text" id="inputEditClientName" class="form-control" name="clientName" ng-model="clientEdit.name" autofocus required/>
</div>
<div class="form-group">
<label class="control-label" for="inputEditClientSecret">{{ 'oidc.client.secret' | tr }}</label>
<input type="text" id="inputEditClientSecret" class="form-control" name="clientSecret" ng-model="clientEdit.secret" required/>
</div>
<div class="form-group">
<label class="control-label" for="inputEditLoginRedirectUri">{{ 'oidc.client.loginRedirectUri' | tr }}</label>
<input type="text" id="inputEditLoginRedirectUri" class="form-control" name="loginRedirectUri" ng-model="clientEdit.loginRedirectUri" required/>
</div>
<div class="form-group">
<label class="control-label">{{ 'oidc.client.signingAlgorithm' | tr }}</label>
<div class="control-label">
<select class="form-control" ng-model="clientEdit.tokenSignatureAlgorithm">
<option value="RS256">RS256</option>
<option value="EdDSA">EdDSA</option>
</select>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="clientEditForm.$invalid"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.close' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="clientEdit.submit()" ng-disabled="clientEditForm.$invalid || clientEdit.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="clientEdit.busy"></i> {{ 'main.dialog.save' | tr }}
</button>
</div>
</div>
</div>
</div>
<!-- Modal client delete -->
<div class="modal fade" id="oidcClientDeleteModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'oidc.deleteClientDialog.title' | tr:{ client: deleteClient.id } }}</h4>
</div>
<div class="modal-body">
<p>{{ 'oidc.deleteClientDialog.description' | tr }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-danger" ng-click="deleteClient.submit()" ng-disabled="deleteClient.busy"><i class="fa fa-circle-notch fa-spin" ng-show="deleteClient.busy"></i> {{ 'main.dialog.delete' | tr }}</button>
</div>
</div>
</div>
</div>
<div class="content content-large">
<div class="text-left">
<h1>
{{ 'users.title' | tr }}
</h1>
</div>
<div class="card card-large">
<form name="profileConfigForm" role="form" novalidate ng-submit="profileConfig.submit()" autocomplete="off">
<fieldset ng-disabled="profileConfig.busy">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="profileConfig.mandatory2FA"> {{ 'users.settings.require2FACheckbox' | tr }}
</label>
</div>
</fieldset>
</form>
<br/>
<div class="row">
<div class="col-md-12">
<span class="has-error" ng-show="profileConfig.errorMessage">{{ profileConfig.errorMessage }}</span>
<button class="btn btn-outline btn-primary pull-right" ng-click="profileConfig.submit()" ng-disabled="!profileConfigForm.$dirty || profileConfig.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="profileConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
</button>
</div>
</div>
</div>
<div class="text-left section-header">
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
</div>
<div class="card card-large">
<div class="row">
<div class="col-md-12">{{ 'users.externalLdap.description' | tr }}</div>
</div>
<br/>
<div class="row" ng-hide="config.features.externalLdap">
<div class="col-md-12">
{{ 'users.externalLdap.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'users.externalLdap.subscriptionRequiredAction' | tr }}</a>
</div>
</div>
<div ng-show="config.features.externalLdap">
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
<div class="col-xs-12">
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.provider }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.url }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.baseDn }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.filter }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row">
<br/>
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="externalLdap.syncBusy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<p ng-show="externalLdap.syncBusy">{{ externalLdap.message }}</p>
<p ng-hide="externalLdap.syncBusy">
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/frontend/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="text-left section-header">
<h3>{{ 'users.exposedLdap.title' | tr }}</h3>
</div>
<div class="card card-large">
<div class="row">
<div class="col-md-12">
<div>{{ 'users.exposedLdap.description' | tr }}</div>
<br/>
<form name="userDirectoryConfigForm" role="form" novalidate ng-submit="userDirectoryConfig.submit()" autocomplete="off">
<fieldset>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
</label>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.secret.url' | tr }}</label>
<div class="input-group">
<input type="text" id="userDirectoryUrlInput" ng-value="'ldaps://' + config.adminFqdn + ':636'" readonly name="userDirectoryUrl" class="form-control"/>
<span class="input-group-btn">
<button class="btn btn-default" type="button" id="userDirectoryUrlClipboardButton" data-clipboard-target="#userDirectoryUrlInput"><i class="fa fa-clipboard"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.secret.label' | tr }}</label>
<p class="small" ng-bind-html=" 'users.exposedLdap.secret.description' | tr:{ userDN: 'cn=admin,ou=system,dc=cloudron' }"></p>
<input type="password" ng-model="userDirectoryConfig.secret" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" name="userDirectorySecret" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.secret.$dirty && userDirectoryConfig.error.secret }" password-reveal/>
<div class="has-error" ng-show="userDirectoryConfig.error.secret">{{ userDirectoryConfig.error.secret }}</div>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.ipRestriction.label' | tr }}</label>
<p class="small">{{ 'users.exposedLdap.ipRestriction.description' | tr }}</p>
<textarea ng-model="userDirectoryConfig.allowlist" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" placeholder="{{ 'users.exposedLdap.ipRestriction.placeholder' | tr }}" name="allowlist" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.allowlist.$dirty && userDirectoryConfig.error.allowlist }" rows="4"></textarea>
<div class="has-error" ng-show="userDirectoryConfig.error.allowlist">{{ userDirectoryConfig.error.allowlist }}</div>
</div>
</fieldset>
</form>
<br/>
<div>
<span class="has-error" ng-show="userDirectoryConfig.error.generic">{{ userDirectoryConfig.error.generic }}</span>
<button class="btn btn-outline btn-primary pull-right" ng-click="userDirectoryConfig.submit()" ng-disabled="!userDirectoryConfigForm.$dirty || userDirectoryConfig.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="userDirectoryConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
</button>
</div>
</div>
</div>
</div>
<div class="text-left section-header">
<h3>{{ 'oidc.title' | tr }}</h3>
</div>
<div class="card card-large">
<div class="grid-item-top">
<div class="row">
<div class="col-md-12">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">{{ 'oidc.env.discoveryUrl' | tr }}</td>
<td class="text-right" style="vertical-align: top;" ng-click-select>https://{{ config.adminFqdn }}/.well-known/openid-configuration</td>
</tr>
</table>
</div>
</div>
</div>
<hr/>
<div>
<h4>{{ 'oidc.clients.title' | tr }} <button class="btn btn-primary btn-sm pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> {{ 'oidc.clients.newClient' | tr }}</button></h4>
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 33%">{{ 'oidc.client.name' | tr }}</th>
<th style="width: 33%">{{ 'oidc.client.id' | tr }}</th>
<th style="width: 33%">{{ 'oidc.client.signingAlgorithm' | tr }}</th>
<th style="width: 10%" class="text-right">{{ 'main.actions' | tr }}</th>
</tr>
</thead>
<tbody>
<tr ng-show="oidcClients.length === 0">
<td colspan="3" class="text-center">{{ 'oidc.clients.empty' | tr }}</td>
</tr>
<tr ng-repeat="client in oidcClients">
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.name }}
</td>
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.id }}
</td>
<td class="text-left elide-table-cell hand" ng-click="clientEdit.show(client)">
{{ client.tokenSignatureAlgorithm }}
</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-danger" ng-click="deleteClient.show(client)" uib-tooltip="Delete"><i class="far fa-trash-alt"></i></button>
<button class="btn btn-xs btn-default" ng-click="clientEdit.show(client)" uib-tooltip="Edit"><i class="fa fa-pencil-alt"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
+448
View File
@@ -0,0 +1,448 @@
'use strict';
/* global angular */
/* global Clipboard */
/* global $ */
angular.module('Application').controller('UserSettingsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
$scope.ldapProvider = [
{ name: 'Active Directory', value: 'ad' },
{ name: 'Cloudron', value: 'cloudron' },
{ name: 'Jumpcloud', value: 'jumpcloud' },
{ name: 'Okta', value: 'okta' },
{ name: 'Univention Corporate Server (UCS)', value: 'univention' },
{ name: 'Other', value: 'other' },
{ name: 'Disabled', value: 'noop' }
];
$translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) {
if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther'];
if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled'];
});
$scope.ready = false;
$scope.config = Client.getConfig();
$scope.userInfo = Client.getUserInfo();
$scope.oidcClients = [];
$scope.profileConfig = {
editableUserProfiles: true,
mandatory2FA: false,
errorMessage: '',
refresh: function () {
Client.getProfileConfig(function (error, result) {
if (error) return console.error('Unable to get directory config.', error);
$scope.profileConfig.editableUserProfiles = !result.lockUserProfiles;
$scope.profileConfig.mandatory2FA = !!result.mandatory2FA;
});
},
submit: function () {
// prevent the current user from getting locked out
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
$scope.profileConfig.error = '';
$scope.profileConfig.busy = true;
$scope.profileConfig.success = false;
var data = {
lockUserProfiles: !$scope.profileConfig.editableUserProfiles,
mandatory2FA: $scope.profileConfig.mandatory2FA
};
Client.setProfileConfig(data, function (error) {
if (error) $scope.profileConfig.errorMessage = error.message;
$scope.profileConfig.success = true;
$scope.profileConfigForm.$setUntouched();
$scope.profileConfigForm.$setPristine();
Client.refreshConfig(); // refresh the $scope.config
$timeout(function () {
$scope.profileConfig.busy = false;
}, 500);
});
}
};
$scope.userDirectoryConfig = {
enabled: false,
secret: '',
allowlist: '',
error: null,
refresh: function () {
Client.getUserDirectoryConfig(function (error, result) {
if (error) return console.error('Unable to get exposed ldap config.', error);
$scope.userDirectoryConfig.enabled = !!result.enabled;
$scope.userDirectoryConfig.allowlist = result.allowlist;
$scope.userDirectoryConfig.secret = result.secret;
});
},
submit: function () {
$scope.userDirectoryConfig.error = null;
$scope.userDirectoryConfig.busy = true;
$scope.userDirectoryConfig.success = false;
var data = {
enabled: $scope.userDirectoryConfig.enabled,
secret: $scope.userDirectoryConfig.secret,
allowlist: $scope.userDirectoryConfig.allowlist
};
Client.setUserDirectoryConfig(data, function (error) {
$scope.userDirectoryConfig.busy = false;
if (error && error.statusCode === 400) {
if (error.message.indexOf('secret') !== -1) return $scope.userDirectoryConfig.error = { secret: error.message };
else return $scope.userDirectoryConfig.error = { allowlist: error.message };
}
if (error) return $scope.userDirectoryConfig.error = { generic: error.message };
$scope.userDirectoryConfigForm.$setUntouched();
$scope.userDirectoryConfigForm.$setPristine();
$scope.userDirectoryConfig.success = true;
});
}
};
$scope.externalLdap = {
busy: false,
percent: 0,
message: '',
errorMessage: '',
error: {},
taskId: 0,
syncBusy: false,
// fields
provider: 'noop',
autoCreate: false,
url: '',
acceptSelfSignedCerts: false,
baseDn: '',
filter: '',
groupBaseDn: '',
bindDn: '',
bindPassword: '',
usernameField: '',
currentConfig: {},
checkStatus: function () {
Client.getLatestTaskByType('syncExternalLdap', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.externalLdap.taskId = task.id;
$scope.externalLdap.updateStatus();
});
},
sync: function () {
$scope.externalLdap.syncBusy = true;
Client.startExternalLdapSync(function (error, taskId) {
if (error) {
$scope.externalLdap.syncBusy = false;
console.error('Unable to start ldap syncer task.', error);
return;
}
$scope.externalLdap.taskId = taskId;
$scope.externalLdap.updateStatus();
});
},
refresh: function() {
Client.getExternalLdapConfig(function (error, result) {
if (error) return console.error('Unable to get external ldap config.', error);
$scope.externalLdap.currentConfig = result;
$scope.externalLdap.checkStatus();
});
},
updateStatus: function () {
Client.getTask($scope.externalLdap.taskId, function (error, data) {
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
if (!data.active) {
$scope.externalLdap.syncBusy = false;
$scope.externalLdap.message = '';
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
return;
}
$scope.externalLdap.syncBusy = true;
$scope.externalLdap.percent = data.percent;
$scope.externalLdap.message = data.message;
window.setTimeout($scope.externalLdap.updateStatus, 3000);
});
},
show: function () {
$scope.externalLdap.busy = false;
$scope.externalLdap.error = {};
$scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider;
$scope.externalLdap.url = $scope.externalLdap.currentConfig.url;
$scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts;
$scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn;
$scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter;
$scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups;
$scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn;
$scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter;
$scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField;
$scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn;
$scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword;
$scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField;
$scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate;
$('#externalLdapModal').modal('show');
},
submit: function () {
$scope.externalLdap.busy = true;
$scope.externalLdap.error = {};
var config = {
provider: $scope.externalLdap.provider
};
if ($scope.externalLdap.provider === 'cloudron') {
config.url = $scope.externalLdap.url;
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
config.autoCreate = $scope.externalLdap.autoCreate;
config.syncGroups = $scope.externalLdap.syncGroups;
config.bindPassword = $scope.externalLdap.bindPassword;
// those values are known and thus overwritten
config.baseDn = 'ou=users,dc=cloudron';
config.filter = '(objectClass=inetOrgPerson)';
config.usernameField = 'username';
config.groupBaseDn = 'ou=groups,dc=cloudron';
config.groupFilter = '(objectClass=group)';
config.groupnameField = 'cn';
config.bindDn = 'cn=admin,ou=system,dc=cloudron';
} else if ($scope.externalLdap.provider !== 'noop') {
config.url = $scope.externalLdap.url;
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
config.baseDn = $scope.externalLdap.baseDn;
config.filter = $scope.externalLdap.filter;
config.usernameField = $scope.externalLdap.usernameField;
config.syncGroups = $scope.externalLdap.syncGroups;
config.groupBaseDn = $scope.externalLdap.groupBaseDn;
config.groupFilter = $scope.externalLdap.groupFilter;
config.groupnameField = $scope.externalLdap.groupnameField;
config.autoCreate = $scope.externalLdap.autoCreate;
if ($scope.externalLdap.bindDn) {
config.bindDn = $scope.externalLdap.bindDn;
config.bindPassword = $scope.externalLdap.bindPassword;
}
}
Client.setExternalLdapConfig(config, function (error) {
$scope.externalLdap.busy = false;
if (error) {
if (error.statusCode === 424) {
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true;
else $scope.externalLdap.error.url = true;
} else if (error.statusCode === 400 && error.message === 'invalid baseDn') {
$scope.externalLdap.error.baseDn = true;
} else if (error.statusCode === 400 && error.message === 'invalid filter') {
$scope.externalLdap.error.filter = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') {
$scope.externalLdap.error.groupBaseDn = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupFilter') {
$scope.externalLdap.error.groupFilter = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupnameField') {
$scope.externalLdap.error.groupnameField = true;
} else if (error.statusCode === 400 && error.message === 'invalid bind credentials') {
$scope.externalLdap.error.credentials = true;
} else if (error.statusCode === 400 && error.message === 'invalid usernameField') {
$scope.externalLdap.error.usernameField = true;
} else {
console.error('Failed to set external LDAP config:', error);
$scope.externalLdap.error.generic = error.message;
}
} else {
$('#externalLdapModal').modal('hide');
$scope.externalLdap.refresh();
}
});
}
};
$scope.refreshOIDCClients = function () {
Client.getOidcClients(function (error, result) {
if (error) return console.error('Failed to load oidc clients', error);
$scope.oidcClients = result;
});
};
$scope.clientAdd = {
busy: false,
error: {},
id: '',
name: '',
secret: '',
loginRedirectUri: '',
tokenSignatureAlgorithm: '',
show: function () {
$scope.clientAdd.id = '';
$scope.clientAdd.secret = '';
$scope.clientAdd.name = '';
$scope.clientAdd.loginRedirectUri = '';
$scope.clientAdd.tokenSignatureAlgorithm = 'RS256';
$scope.clientAdd.busy = false;
$scope.clientAdd.error = null;
$scope.clientAddForm.$setPristine();
$('#oidcClientAddModal').modal('show');
},
submit: function () {
$scope.clientAdd.busy = true;
$scope.clientAdd.error = {};
Client.addOidcClient($scope.clientAdd.id, $scope.clientAdd.name, $scope.clientAdd.secret, $scope.clientAdd.loginRedirectUri, $scope.clientAdd.tokenSignatureAlgorithm, function (error) {
if (error) {
if (error.statusCode === 409) {
$scope.clientAdd.error.id = 'Client ID already exists';
$('#clientId').focus();
} else {
console.error('Unable to add openid client.', error);
}
$scope.clientAdd.busy = false;
return;
}
$scope.refreshOIDCClients();
$scope.clientAdd.busy = false;
$('#oidcClientAddModal').modal('hide');
});
}
};
$scope.clientEdit = {
busy: false,
error: {},
id: '',
name: '',
secret: '',
loginRedirectUri: '',
tokenSignatureAlgorithm: '',
show: function (client) {
$scope.clientEdit.id = client.id;
$scope.clientEdit.name = client.name;
$scope.clientEdit.secret = client.secret;
$scope.clientEdit.loginRedirectUri = client.loginRedirectUri;
$scope.clientEdit.tokenSignatureAlgorithm = client.tokenSignatureAlgorithm;
$scope.clientEdit.busy = false;
$scope.clientEdit.error = null;
$scope.clientEditForm.$setPristine();
$('#oidcClientEditModal').modal('show');
},
submit: function () {
$scope.clientEdit.busy = true;
$scope.clientEdit.error = {};
Client.updateOidcClient($scope.clientEdit.id, $scope.clientEdit.name, $scope.clientEdit.secret, $scope.clientEdit.loginRedirectUri, $scope.clientEdit.tokenSignatureAlgorithm, function (error) {
if (error) {
console.error('Unable to edit openid client.', error);
$scope.clientEdit.busy = false;
return;
}
$scope.refreshOIDCClients();
$scope.clientEdit.busy = false;
$('#oidcClientEditModal').modal('hide');
});
}
};
$scope.deleteClient = {
busy: false,
error: {},
id: '',
show: function (client) {
$scope.deleteClient.busy = false;
$scope.deleteClient.id = client.id;
$('#oidcClientDeleteModal').modal('show');
},
submit: function () {
Client.delOidcClient($scope.deleteClient.id, function (error) {
$scope.deleteClient.busy = false;
if (error) return console.error('Failed to delete openid client', error);
$scope.refreshOIDCClients();
$('#oidcClientDeleteModal').modal('hide');
});
}
};
Client.onReady(function () {
$scope.externalLdap.refresh();
$scope.profileConfig.refresh();
$scope.userDirectoryConfig.refresh();
$scope.refreshOIDCClients();
});
// setup all the dialog focus handling
['oidcClientAddModal', 'oidcClientEditModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
new Clipboard('#userDirectoryUrlClipboardButton').on('success', function(e) {
$('#userDirectoryUrlClipboardButton').tooltip({
title: 'Copied!',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
e.clearSelection();
}).on('error', function(/*e*/) {
$('#userDirectoryUrlClipboardButton').tooltip({
title: 'Press Ctrl+C to copy',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
});
$('.modal-backdrop').remove();
}]);
+2 -399
View File
@@ -1,39 +1,3 @@
<!-- Modal subscription -->
<div class="modal fade" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'users.subscriptionDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<p>To add more users, please setup a paid plain.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'users.subscriptionDialog.setupAction' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal subscription group -->
<div class="modal fade" id="subscriptionRequiredGroupModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'users.subscriptionDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<p>User groups are part of the business plan.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-success" ng-click="openSubscriptionSetup()">{{ 'users.subscriptionDialog.setupAction' | tr }}</button>
</div>
</div>
</div>
</div>
<!-- Modal make user local -->
<div class="modal fade" id="makeLocalModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -483,112 +447,11 @@
</div>
</div>
<!-- Modal external ldap -->
<div class="modal fade" id="externalLdapModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ 'users.externalLdapDialog.title' | tr }}</h4>
</div>
<div class="modal-body">
<p class="has-error text-center" ng-show="externalLdap.error.generic">{{ externalLdap.error.generic }}</p>
<div class="form-group">
<label class="control-label" for="ldapProvider">{{ 'users.externalLdap.provider' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#external-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" id="ldapProvider" ng-model="externalLdap.provider" ng-options="a.value as a.name for a in ldapProvider"></select>
</div>
<div uib-collapse="externalLdap.provider === 'noop'">
<form name="externalLdapConfigForm" role="form" novalidate ng-submit="externalLdap.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.url }">
<label class="control-label" for="inputExternalLdapConfigUrl">{{ 'users.externalLdap.server' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.url" id="inputExternalLdapConfigUrl" name="url" ng-disabled="externalLdap.busy" placeholder="ldaps://example.com:636" required>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.acceptSelfSignedCerts"> {{ 'users.externalLdap.acceptSelfSignedCert' | tr }}
</label>
</div>
<p class="has-error" ng-show="externalLdap.error.acceptSelfSignedCerts">{{ 'users.externalLdap.errorSelfSignedCert' | tr }}</p>
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.baseDn }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigBaseDn">{{ 'users.externalLdap.baseDn' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.baseDn" id="inputExternalLdapConfigBaseDn" name="baseDn" ng-disabled="externalLdap.busy" placeholder="ou=users,dc=example,dc=com" ng-required="externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.filter }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigFilter">{{ 'users.externalLdap.filter' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.filter" id="inputExternalLdapConfigFilter" name="filter" ng-disabled="externalLdap.busy" placeholder="(objectClass=inetOrgPerson)" ng-required="externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.usernameField }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigUsernameField">{{ 'users.externalLdap.usernameField' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.usernameField" id="inputExternalLdapConfigUsernameField" name="usernameField" ng-disabled="externalLdap.busy" placeholder="uid or sAMAcountName">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.syncGroups"> {{ 'users.externalLdap.syncGroups' | tr }}</sup>
</label>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupBaseDn }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupBaseDn">{{ 'users.externalLdap.groupBaseDn' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupBaseDn" id="inputExternalLdapConfigGroupBaseDn" name="groupBaseDn" ng-disabled="externalLdap.busy" placeholder="ou=groups,dc=example,dc=com" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupFilter }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupFilter">{{ 'users.externalLdap.groupFilter' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupFilter" id="inputExternalLdapConfigGroupFilter" name="groupFilter" ng-disabled="externalLdap.busy" placeholder="(objectClass=groupOfNames)" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.groupnameField }" ng-show="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigGroupnameField">{{ 'users.externalLdap.groupnameField' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.groupnameField" id="inputExternalLdapConfigGroupnameField" name="groupnameField" ng-disabled="externalLdap.busy" placeholder="cn" ng-required="externalLdap.syncGroups && externalLdap.provider !== 'cloudron'">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }" ng-show="externalLdap.provider !== 'cloudron'">
<label class="control-label" for="inputExternalLdapConfigBindDn">{{ 'users.externalLdap.bindUsername' | tr }}</label>
<input type="text" class="form-control" ng-model="externalLdap.bindDn" id="inputExternalLdapConfigBindDn" name="bindDn" ng-disabled="externalLdap.busy" placeholder="uid=admin,ou=Users,dc=example,dc=com">
</div>
<div class="form-group" ng-class="{ 'has-error': externalLdap.error.credentials }">
<label class="control-label" for="inputExternalLdapConfigBindPassword">{{ 'users.externalLdap.bindPassword' | tr }}</label>
<input type="password" class="form-control" ng-model="externalLdap.bindPassword" id="inputExternalLdapConfigBindPassword" name="bindPassword" ng-disabled="externalLdap.busy" placeholder="" password-reveal>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="externalLdap.autoCreate"> {{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}
</label>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="externalLdapConfigForm.$invalid"/>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'main.dialog.cancel' | tr }}</button>
<button type="button" class="btn btn-primary" ng-click="externalLdap.submit()" ng-disabled="externalLdapConfigForm.$invalid || externalLdap.busy"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.busy"></i> {{ 'main.dialog.save' | tr }}</button>
</div>
</div>
</div>
</div>
<div class="content content-large">
<div class="text-left">
<h1>
{{ 'users.title' | tr }}
{{ 'main.navbar.users' | tr }}
</h1>
</div>
@@ -672,6 +535,7 @@
</div>
<div class="pull-right">
<button class="btn btn-default btn-outline btn-xs" ng-click="showPrevPage()" ng-class="{ 'btn-primary': currentPage > 1 }" ng-disabled="userRefreshBusy || currentPage <= 1"><i class="fa fa-angle-double-left"></i> {{ 'main.pagination.prev' | tr }}</button>
<span style="margin: 0 5px; line-height: 1.5; font-size: 12px;">{{ currentPage }}</span>
<button class="btn btn-default btn-outline btn-xs" ng-click="showNextPage()" ng-class="{ 'btn-primary': users.length > pageItems }" ng-disabled="userRefreshBusy || users.length < pageItems">{{ 'main.pagination.next' | tr }} <i class="fa fa-angle-double-right"></i></button>
</div>
</div>
@@ -727,265 +591,4 @@
</div>
</div>
</div>
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
<h3>{{ 'users.settings.title' | tr }}</h3>
</div>
<div class="card card-large" ng-show="user.isAtLeastAdmin">
<form name="profileConfigForm" role="form" novalidate ng-submit="profileConfig.submit()" autocomplete="off">
<fieldset ng-disabled="profileConfig.busy">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="profileConfig.editableUserProfiles"> {{ 'users.settings.allowProfileEditCheckbox' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#lock-profile" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="profileConfig.mandatory2FA"> {{ 'users.settings.require2FACheckbox' | tr }}
</label>
</div>
</fieldset>
</form>
<br/>
<div class="row">
<div class="col-md-12">
<span class="has-error" ng-show="profileConfig.errorMessage">{{ profileConfig.errorMessage }}</span>
<button class="btn btn-outline btn-primary pull-right" ng-click="profileConfig.submit()" ng-disabled="!profileConfigForm.$dirty || profileConfig.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="profileConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
</button>
</div>
</div>
</div>
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
<h3>{{ 'users.externalLdap.title' | tr }}</h3>
</div>
<div class="card card-large" ng-show="user.isAtLeastAdmin">
<div class="row">
<div class="col-md-12">{{ 'users.externalLdap.description' | tr }}</div>
</div>
<br/>
<div class="row" ng-hide="config.features.externalLdap">
<div class="col-md-12">
{{ 'users.externalLdap.subscriptionRequired' | tr }} <a href="" class="pull-right" ng-click="openSubscriptionSetup()">{{ 'users.externalLdap.subscriptionRequiredAction' | tr }}</a>
</div>
</div>
<div ng-show="config.features.externalLdap">
<div class="row" ng-show="externalLdap.currentConfig.provider === 'noop'">
<div class="col-xs-12">
<span class="text-muted">{{ 'users.externalLdap.noopInfo' | tr }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.provider' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.provider }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.server' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.url }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.acceptSelfSignedCert' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.acceptSelfSignedCerts ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.baseDn' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.baseDn }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.filter' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.filter }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.usernameField' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.usernameField || 'uid' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.syncGroups' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.syncGroups ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupBaseDn' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupBaseDn }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupFilter' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupFilter }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron' && externalLdap.currentConfig.syncGroups">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.groupnameField' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.groupnameField }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop' && externalLdap.currentConfig.provider !== 'cloudron'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.auth' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.bindDn ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row" ng-show="externalLdap.currentConfig.provider !== 'noop'">
<div class="col-xs-6">
<span class="text-muted">{{ 'users.externalLdap.autocreateUsersOnLogin' | tr }}</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ externalLdap.currentConfig.autoCreate ? 'Yes' : 'No' }}</span>
</div>
</div>
<div class="row">
<br/>
<div class="col-md-12" style="margin-bottom: 10px;">
<div ng-show="externalLdap.syncBusy" class="progress progress-striped active animateMe">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ externalLdap.percent }}%"></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<p ng-show="externalLdap.syncBusy">{{ externalLdap.message }}</p>
<p ng-hide="externalLdap.syncBusy">
<div class="has-error" ng-show="!externalLdap.active">{{ externalLdap.errorMessage }}</div>
</p>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-primary pull-right" ng-click="externalLdap.show()">{{ 'users.externalLdap.configureAction' | tr }}</button>
<button class="btn btn-success pull-right" ng-disabled="externalLdap.currentConfig.provider === 'noop'" ng-click="externalLdap.sync()"><i class="fa fa-circle-notch fa-spin" ng-show="externalLdap.syncBusy"></i> {{ 'users.externalLdap.syncAction' | tr }}</button>
<a class="btn btn-primary pull-right" ng-show="externalLdap.taskId" ng-href="/logs.html?taskId={{ externalLdap.taskId }}" target="_blank">{{ 'users.externalLdap.showLogsAction' | tr }}</a>
</div>
</div>
</div>
</div>
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
<h3>{{ 'users.exposedLdap.title' | tr }}</h3>
</div>
<div class="card card-large" ng-show="user.isAtLeastAdmin">
<div class="row">
<div class="col-md-12">
<div>{{ 'users.exposedLdap.description' | tr }}</div>
<br/>
<form name="userDirectoryConfigForm" role="form" novalidate ng-submit="userDirectoryConfig.submit()" autocomplete="off">
<fieldset>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="userDirectoryConfig.enabled" ng-disabled="userDirectoryConfig.busy"> {{ 'users.exposedLdap.enabled' | tr }} <sup><a ng-href="https://docs.cloudron.io/user-management/#directory-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup>
</label>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.secret.url' | tr }}</label>
<div class="input-group">
<input type="text" id="userDirectoryUrlInput" ng-value="'ldaps://' + config.adminFqdn + ':636'" readonly name="userDirectoryUrl" class="form-control"/>
<span class="input-group-btn">
<button class="btn btn-default" type="button" id="userDirectoryUrlClipboardButton" data-clipboard-target="#userDirectoryUrlInput"><i class="fa fa-clipboard"></i></button>
</span>
</div>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.secret.label' | tr }}</label>
<p class="small" ng-bind-html=" 'users.exposedLdap.secret.description' | tr:{ userDN: 'cn=admin,ou=system,dc=cloudron' }"></p>
<input type="password" ng-model="userDirectoryConfig.secret" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" name="userDirectorySecret" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.secret.$dirty && userDirectoryConfig.error.secret }" password-reveal/>
<div class="has-error" ng-show="userDirectoryConfig.error.secret">{{ userDirectoryConfig.error.secret }}</div>
</div>
<div class="form-group">
<label class="control-label">{{ 'users.exposedLdap.ipRestriction.label' | tr }}</label>
<p class="small">{{ 'users.exposedLdap.ipRestriction.description' | tr }}</p>
<textarea ng-model="userDirectoryConfig.allowlist" ng-disabled="!userDirectoryConfig.enabled || userDirectoryConfig.busy" placeholder="{{ 'users.exposedLdap.ipRestriction.placeholder' | tr }}" name="allowlist" class="form-control" ng-class="{ 'has-error': !userDirectoryConfigForm.allowlist.$dirty && userDirectoryConfig.error.allowlist }" rows="4"></textarea>
<div class="has-error" ng-show="userDirectoryConfig.error.allowlist">{{ userDirectoryConfig.error.allowlist }}</div>
</div>
</fieldset>
</form>
<br/>
<div>
<span class="has-error" ng-show="userDirectoryConfig.error.generic">{{ userDirectoryConfig.error.generic }}</span>
<button class="btn btn-outline btn-primary pull-right" ng-click="userDirectoryConfig.submit()" ng-disabled="!userDirectoryConfigForm.$dirty || userDirectoryConfig.busy">
<i class="fa fa-circle-notch fa-spin" ng-show="userDirectoryConfig.busy"></i> {{ 'users.settings.saveAction' | tr }}
</button>
</div>
</div>
</div>
</div>
<div class="text-left" style="margin-top: 50px;" ng-show="user.isAtLeastAdmin">
<h3>{{ 'oidc.title' | tr }}</h3>
</div>
<div class="card card-large" ng-show="user.isAtLeastAdmin">
<div class="row">
<div class="col-md-12" style="line-height: 34px;">
{{ 'oidc.description' | tr }}
<a href="/#/oidc" class="btn btn-outline btn-primary pull-right">{{ 'main.settings' | tr }}</a>
</div>
</div>
</div>
</div>
+2 -318
View File
@@ -9,21 +9,6 @@
angular.module('Application').controller('UsersController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); });
$scope.ldapProvider = [
{ name: 'Active Directory', value: 'ad' },
{ name: 'Cloudron', value: 'cloudron' },
{ name: 'Jumpcloud', value: 'jumpcloud' },
{ name: 'Okta', value: 'okta' },
{ name: 'Univention Corporate Server (UCS)', value: 'univention' },
{ name: 'Other', value: 'other' },
{ name: 'Disabled', value: 'noop' }
];
$translate(['users.externalLdap.providerOther', 'users.externalLdap.providerDisabled']).then(function (tr) {
if (tr['users.externalLdap.providerOther']) $scope.ldapProvider.find(function (p) { return p.value === 'other'; }).name = tr['users.externalLdap.providerOther'];
if (tr['users.externalLdap.providerDisabled']) $scope.ldapProvider.find(function (p) { return p.value === 'noop'; }).name = tr['users.externalLdap.providerDisabled'];
});
$scope.ready = false;
$scope.users = []; // users of current page
$scope.allUsersById = [];
@@ -31,18 +16,13 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$scope.groupsById = { };
$scope.config = Client.getConfig();
$scope.userInfo = Client.getUserInfo();
$scope.domains = [];
$scope.openSubscriptionSetup = function () {
Client.openSubscriptionSetup($scope.$parent.subscription);
};
$scope.roles = [];
$scope.allUsers = []; // all the users and not just current page, have to load this for group assignment
$scope.userSearchString = '';
$scope.currentPage = 1;
$scope.pageItems = 15;
$scope.pageItems = localStorage.cloudronPageSize || 15;
$scope.userRefreshBusy = true;
$scope.userStates = [
@@ -845,263 +825,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
}
};
$scope.profileConfig = {
editableUserProfiles: true,
mandatory2FA: false,
errorMessage: '',
refresh: function () {
Client.getProfileConfig(function (error, result) {
if (error) return console.error('Unable to get directory config.', error);
$scope.profileConfig.editableUserProfiles = !result.lockUserProfiles;
$scope.profileConfig.mandatory2FA = !!result.mandatory2FA;
});
},
submit: function () {
// prevent the current user from getting locked out
if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile');
$scope.profileConfig.error = '';
$scope.profileConfig.busy = true;
$scope.profileConfig.success = false;
var data = {
lockUserProfiles: !$scope.profileConfig.editableUserProfiles,
mandatory2FA: $scope.profileConfig.mandatory2FA
};
Client.setProfileConfig(data, function (error) {
if (error) $scope.profileConfig.errorMessage = error.message;
$scope.profileConfig.success = true;
$scope.profileConfigForm.$setUntouched();
$scope.profileConfigForm.$setPristine();
Client.refreshConfig(); // refresh the $scope.config
$timeout(function () {
$scope.profileConfig.busy = false;
}, 500);
});
}
};
$scope.userDirectoryConfig = {
enabled: false,
secret: '',
allowlist: '',
error: null,
refresh: function () {
Client.getUserDirectoryConfig(function (error, result) {
if (error) return console.error('Unable to get exposed ldap config.', error);
$scope.userDirectoryConfig.enabled = !!result.enabled;
$scope.userDirectoryConfig.allowlist = result.allowlist;
$scope.userDirectoryConfig.secret = result.secret;
});
},
submit: function () {
$scope.userDirectoryConfig.error = null;
$scope.userDirectoryConfig.busy = true;
$scope.userDirectoryConfig.success = false;
var data = {
enabled: $scope.userDirectoryConfig.enabled,
secret: $scope.userDirectoryConfig.secret,
allowlist: $scope.userDirectoryConfig.allowlist
};
Client.setUserDirectoryConfig(data, function (error) {
$scope.userDirectoryConfig.busy = false;
if (error && error.statusCode === 400) {
if (error.message.indexOf('secret') !== -1) return $scope.userDirectoryConfig.error = { secret: error.message };
else return $scope.userDirectoryConfig.error = { allowlist: error.message };
}
if (error) return $scope.userDirectoryConfig.error = { generic: error.message };
$scope.userDirectoryConfigForm.$setUntouched();
$scope.userDirectoryConfigForm.$setPristine();
$scope.userDirectoryConfig.success = true;
});
}
};
$scope.externalLdap = {
busy: false,
percent: 0,
message: '',
errorMessage: '',
error: {},
taskId: 0,
syncBusy: false,
// fields
provider: 'noop',
autoCreate: false,
url: '',
acceptSelfSignedCerts: false,
baseDn: '',
filter: '',
groupBaseDn: '',
bindDn: '',
bindPassword: '',
usernameField: '',
currentConfig: {},
checkStatus: function () {
Client.getLatestTaskByType('syncExternalLdap', function (error, task) {
if (error) return console.error(error);
if (!task) return;
$scope.externalLdap.taskId = task.id;
$scope.externalLdap.updateStatus();
});
},
sync: function () {
$scope.externalLdap.syncBusy = true;
Client.startExternalLdapSync(function (error, taskId) {
if (error) {
$scope.externalLdap.syncBusy = false;
console.error('Unable to start ldap syncer task.', error);
return;
}
$scope.externalLdap.taskId = taskId;
$scope.externalLdap.updateStatus();
});
},
updateStatus: function () {
Client.getTask($scope.externalLdap.taskId, function (error, data) {
if (error) return window.setTimeout($scope.externalLdap.updateStatus, 5000);
if (!data.active) {
$scope.externalLdap.syncBusy = false;
$scope.externalLdap.message = '';
$scope.externalLdap.percent = 100; // indicates that 'result' is valid
$scope.externalLdap.errorMessage = data.success ? '' : data.error.message;
refreshGroups();
refreshUsers();
return;
}
$scope.externalLdap.syncBusy = true;
$scope.externalLdap.percent = data.percent;
$scope.externalLdap.message = data.message;
window.setTimeout($scope.externalLdap.updateStatus, 3000);
});
},
show: function () {
$scope.externalLdap.busy = false;
$scope.externalLdap.error = {};
$scope.externalLdap.provider = $scope.externalLdap.currentConfig.provider;
$scope.externalLdap.url = $scope.externalLdap.currentConfig.url;
$scope.externalLdap.acceptSelfSignedCerts = $scope.externalLdap.currentConfig.acceptSelfSignedCerts;
$scope.externalLdap.baseDn = $scope.externalLdap.currentConfig.baseDn;
$scope.externalLdap.filter = $scope.externalLdap.currentConfig.filter;
$scope.externalLdap.syncGroups = $scope.externalLdap.currentConfig.syncGroups;
$scope.externalLdap.groupBaseDn = $scope.externalLdap.currentConfig.groupBaseDn;
$scope.externalLdap.groupFilter = $scope.externalLdap.currentConfig.groupFilter;
$scope.externalLdap.groupnameField = $scope.externalLdap.currentConfig.groupnameField;
$scope.externalLdap.bindDn = $scope.externalLdap.currentConfig.bindDn;
$scope.externalLdap.bindPassword = $scope.externalLdap.currentConfig.bindPassword;
$scope.externalLdap.usernameField = $scope.externalLdap.currentConfig.usernameField;
$scope.externalLdap.autoCreate = $scope.externalLdap.currentConfig.autoCreate;
$('#externalLdapModal').modal('show');
},
submit: function () {
$scope.externalLdap.busy = true;
$scope.externalLdap.error = {};
var config = {
provider: $scope.externalLdap.provider
};
if ($scope.externalLdap.provider === 'cloudron') {
config.url = $scope.externalLdap.url;
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
config.autoCreate = $scope.externalLdap.autoCreate;
config.syncGroups = $scope.externalLdap.syncGroups;
config.bindPassword = $scope.externalLdap.bindPassword;
// those values are known and thus overwritten
config.baseDn = 'ou=users,dc=cloudron';
config.filter = '(objectClass=inetOrgPerson)';
config.usernameField = 'username';
config.groupBaseDn = 'ou=groups,dc=cloudron';
config.groupFilter = '(objectClass=group)';
config.groupnameField = 'cn';
config.bindDn = 'cn=admin,ou=system,dc=cloudron';
} else if ($scope.externalLdap.provider !== 'noop') {
config.url = $scope.externalLdap.url;
config.acceptSelfSignedCerts = $scope.externalLdap.acceptSelfSignedCerts;
config.baseDn = $scope.externalLdap.baseDn;
config.filter = $scope.externalLdap.filter;
config.usernameField = $scope.externalLdap.usernameField;
config.syncGroups = $scope.externalLdap.syncGroups;
config.groupBaseDn = $scope.externalLdap.groupBaseDn;
config.groupFilter = $scope.externalLdap.groupFilter;
config.groupnameField = $scope.externalLdap.groupnameField;
config.autoCreate = $scope.externalLdap.autoCreate;
if ($scope.externalLdap.bindDn) {
config.bindDn = $scope.externalLdap.bindDn;
config.bindPassword = $scope.externalLdap.bindPassword;
}
}
Client.setExternalLdapConfig(config, function (error) {
$scope.externalLdap.busy = false;
if (error) {
if (error.statusCode === 424) {
if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') $scope.externalLdap.error.acceptSelfSignedCerts = true;
else $scope.externalLdap.error.url = true;
} else if (error.statusCode === 400 && error.message === 'invalid baseDn') {
$scope.externalLdap.error.baseDn = true;
} else if (error.statusCode === 400 && error.message === 'invalid filter') {
$scope.externalLdap.error.filter = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupBaseDn') {
$scope.externalLdap.error.groupBaseDn = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupFilter') {
$scope.externalLdap.error.groupFilter = true;
} else if (error.statusCode === 400 && error.message === 'invalid groupnameField') {
$scope.externalLdap.error.groupnameField = true;
} else if (error.statusCode === 400 && error.message === 'invalid bind credentials') {
$scope.externalLdap.error.credentials = true;
} else if (error.statusCode === 400 && error.message === 'invalid usernameField') {
$scope.externalLdap.error.usernameField = true;
} else {
console.error('Failed to set external LDAP config:', error);
$scope.externalLdap.error.generic = error.message;
}
} else {
$('#externalLdapModal').modal('hide');
loadExternalLdapConfig();
}
});
}
};
function getUsers(callback) {
var users = [];
@@ -1159,15 +882,6 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
});
}
function loadExternalLdapConfig() {
Client.getExternalLdapConfig(function (error, result) {
if (error) return console.error('Unable to get external ldap config.', error);
$scope.externalLdap.currentConfig = result;
$scope.externalLdap.checkStatus();
});
}
$scope.showNextPage = function () {
$scope.currentPage++;
refreshUsers();
@@ -1196,20 +910,8 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
});
}
function getAllDomains() {
Client.getDomains(function (error, domains) {
if (error) return console.error('Unable to get domains:', error);
$scope.domains = domains;
});
}
Client.onReady(function () {
refresh();
if ($scope.user.isAtLeastAdmin) loadExternalLdapConfig();
if ($scope.user.isAtLeastAdmin) $scope.profileConfig.refresh();
if ($scope.user.isAtLeastAdmin) $scope.userDirectoryConfig.refresh();
if ($scope.user.isAtLeastAdmin) getAllDomains();
refreshAllUsers();
// Order matters for permissions used in canEdit
@@ -1228,7 +930,7 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
// setup all the dialog focus handling
['userAddModal', 'userRemoveModal', 'userEditModal', 'groupAddModal', 'groupEditModal', 'groupRemoveModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
$(this).find('[autofocus]:first').focus();
});
});
@@ -1286,23 +988,5 @@ angular.module('Application').controller('UsersController', ['$scope', '$locatio
$timeout(function () { $('#setGhostClipboardButton').tooltip('hide'); }, 2000);
});
new Clipboard('#userDirectoryUrlClipboardButton').on('success', function(e) {
$('#userDirectoryUrlClipboardButton').tooltip({
title: 'Copied!',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
e.clearSelection();
}).on('error', function(/*e*/) {
$('#userDirectoryUrlClipboardButton').tooltip({
title: 'Press Ctrl+C to copy',
trigger: 'manual'
}).tooltip('show');
$timeout(function () { $('#userDirectoryUrlClipboardButton').tooltip('hide'); }, 2000);
});
$('.modal-backdrop').remove();
}]);
+8 -4
View File
@@ -34,10 +34,14 @@
<input type="text" class="form-control" ng-model="volumeAdd.hostPath" name="hostPath" ng-disabled="volumeAdd.busy" placeholder="/mnt/data" ng-required="volumeAdd.mountType === 'mountpoint'" autofocus>
</div>
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs'">
<div class="form-group" ng-show="volumeAdd.mountType === 'ext4'">
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
<select class="form-control" ng-model="volumeAdd.diskPath" ng-options="item.path as item.label for item in blockDevices track by item.path"></select>
<input type="text" class="form-control" style="margin-top: 5px;" ng-show="volumeAdd.diskPath.path === 'custom'" ng-model="volumeAdd.customDiskPath" ng-disabled="volumeAdd.busy" placeholder="/dev/disk/by-uuid/uuid" ng-required="(volumeAdd.mountType === 'ext4' || volumeAdd.mountType === 'xfs') && volumeAdd.diskPath.path === 'custom'">
<select class="form-control" ng-model="volumeAdd.ext4Disk" ng-options="item as item.label for item in ext4BlockDevices track by item.path" ng-required="volumeAdd.mountType === 'ext4'"></select>
</div>
<div class="form-group" ng-show="volumeAdd.mountType === 'xfs'">
<label class="control-label">{{ 'volumes.addVolumeDialog.diskPath' | tr }}</label>
<select class="form-control" ng-model="volumeAdd.xfsDisk" ng-options="item as item.label for item in xfsBlockDevices track by item.path" ng-required="volumeAdd.mountType === 'xfs'"></select>
</div>
<div class="form-group" ng-show="volumeAdd.mountType === 'cifs' || volumeAdd.mountType === 'nfs' || volumeAdd.mountType === 'sshfs'">
@@ -153,7 +157,7 @@
<td class="text-left wrap-table-cell hidden-xs hidden-sm" ng-show="volume.mountType === 'mountpoint' || volume.mountType === 'filesystem'">{{ volume.hostPath }}</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="remount(volume)" ng-show="isMountProvider(volume.mountType)" ng-disabled="volume.remounting" uib-tooltip="{{ 'volumes.remountActionTooltip' | tr }}"><i class="fa fa-sync-alt" ng-class="{ 'fa-spin': volume.remounting }"></i></button>
<a class="btn btn-xs btn-default" ng-href="{{ '/filemanager.html?type=volume&id=' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
<a class="btn btn-xs btn-default" ng-href="{{ '/frontend/filemanager.html#/home/volume/' + volume.id }}" target="_blank" uib-tooltip="{{ 'volumes.openFileManagerActionTooltip' | tr }}"><i class="fas fa-folder"></i></a>
<button class="btn btn-xs btn-danger" ng-click="volumeRemove.show(volume)" uib-tooltip="{{ 'volumes.removeVolumeActionTooltip' | tr }}"><i class="far fa-trash-alt"></i></button>
</td>
</tr>
+15 -13
View File
@@ -5,7 +5,7 @@
/* global async */
angular.module('Application').controller('VolumesController', ['$scope', '$location', '$timeout', 'Client', function ($scope, $location, $timeout, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastUserManager) $location.path('/'); });
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
var refreshVolumesTimerId = null;
@@ -88,8 +88,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
remoteDir: '',
username: '',
password: '',
diskPath: {}, // { path, type }
customDiskPath: '',
ext4Disk: null, // { path, type }
xfsDisk: null, // { path, type }
user: '',
seal: false,
port: 22,
@@ -105,8 +105,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
$scope.volumeAdd.remoteDir = '';
$scope.volumeAdd.username = '';
$scope.volumeAdd.password = '';
$scope.volumeAdd.diskPath = {};
$scope.volumeAdd.customDiskPath = '';
$scope.volumeAdd.ext4Disk = null;
$scope.volumeAdd.xfsDisk = null;
$scope.volumeAdd.user = '';
$scope.volumeAdd.seal = false;
$scope.volumeAdd.port = 22;
@@ -119,7 +119,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
show: function () {
$scope.volumeAdd.reset();
$scope.blockDevices = [];
$scope.ext4BlockDevices = [];
$scope.xfsBlockDevices = [];
Client.getBlockDevices(function (error, result) {
if (error) console.error('Failed to list blockdevices:', error);
@@ -130,11 +131,8 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
// amend label for UI
result.forEach(function (d) { d.label = d.path; });
// add custom fake option
result.push({ path: 'custom', label: 'Custom Path' });
$scope.blockDevices = result;
$scope.volumeAdd.diskPath = $scope.blockDevices[0];
$scope.ext4BlockDevices = result.filter(function (d) { return d.type === 'ext4'; });
$scope.xfsBlockDevices = result.filter(function (d) { return d.type === 'xfs'; });
$('#volumeAddModal').modal('show');
});
@@ -167,9 +165,13 @@ angular.module('Application').controller('VolumesController', ['$scope', '$locat
user: $scope.volumeAdd.user,
privateKey: $scope.volumeAdd.privateKey,
};
} else if ($scope.volumeAdd.mountType === 'ext4' || $scope.volumeAdd.mountType === 'xfs') {
} else if ($scope.volumeAdd.mountType === 'ext4') {
mountOptions = {
diskPath: $scope.volumeAdd.diskPath === 'custom' ? $scope.volumeAdd.customDiskPath : $scope.volumeAdd.diskPath
diskPath: $scope.volumeAdd.ext4Disk.path
};
} else if ($scope.volumeAdd.mountType === 'xfs') {
mountOptions = {
diskPath: $scope.volumeAdd.xfsDisk.path
};
} else if ($scope.volumeAdd.mountType === 'mountpoint' || $scope.volumeAdd.mountType === 'filesystem') {
mountOptions = {
-7
View File
@@ -1,7 +0,0 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
./node_modules/.bin/vite build --base=/filemanager/
-25
View File
@@ -1,25 +0,0 @@
{
"name": "my-vue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"combokeys": "^3.0.1",
"filesize": "^10.0.7",
"pankow": "^0.1.2",
"primeicons": "^6.0.1",
"primevue": "^3.27.0",
"superagent": "^8.0.9",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.1",
"vite": "^4.3.3"
}
}
-35
View File
@@ -1,35 +0,0 @@
import { createApp } from 'vue';
import './style.css';
import 'primevue/resources/themes/saga-blue/theme.css';
import 'primevue/resources/primevue.min.css';
import 'primeicons/primeicons.css';
import PrimeVue from 'primevue/config';
import ConfirmationService from 'primevue/confirmationservice';
import { createRouter, createWebHashHistory } from 'vue-router';
import App from './App.vue';
import Home from './views/Home.vue';
import Viewer from './views/Viewer.vue';
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home/:type?/:resourceId?/:cwd*', component: Home },
{ path: '/viewer/:type/:resourceId/:filePath*', component: Viewer }
];
const router = createRouter({
// 4. Provide the history implementation to use. We are using the hash history for simplicity here.
history: createWebHashHistory(),
routes,
});
const app = createApp(App);
app.use(router);
app.use(PrimeVue, { ripple: true });
app.use(ConfirmationService);
app.mount('#app');
-196
View File
@@ -1,196 +0,0 @@
import { filesize } from 'filesize';
function prettyDate(value) {
var date = new Date(value),
diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return;
return day_diff === 0 && (
diff < 60 && 'just now' ||
diff < 120 && '1 min ago' ||
diff < 3600 && Math.floor( diff / 60 ) + ' min ago' ||
diff < 7200 && '1 hour ago' ||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
day_diff === 1 && 'Yesterday' ||
day_diff < 7 && day_diff + ' days ago' ||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
Math.round( day_diff / 365 ) + ' years ago';
}
function prettyLongDate(value) {
if (!value) return 'unkown';
var date = new Date(value);
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
function prettyFileSize(value) {
if (typeof value !== 'number') return 'unkown';
return filesize(value);
}
function sanitize(path) {
path = '/' + path;
return path.replace(/\/+/g, '/');
}
function encode(path) {
return path.split('/').map(encodeURIComponent).join('/');
}
function decode(path) {
return path.split('/').map(decodeURIComponent).join('/');
}
// TODO create share links instead of using access token
function getDirectLink(entry) {
if (entry.share) {
let link = window.location.origin + '/api/v1/shares/' + entry.share.id + '?type=raw&path=' + encodeURIComponent(entry.filePath);
return link;
} else {
return window.location.origin + '/api/v1/files?type=raw&path=' + encodeURIComponent(entry.filePath);
}
}
// TODO the url might actually return a 412 in which case we have to keep reloading
function getPreviewUrl(entry) {
if (!entry.previewUrl) return '';
return entry.previewUrl;
}
function getShareLink(shareId) {
return window.location.origin + '/api/v1/shares/' + shareId + '?type=raw';
}
function download(entries, name) {
if (!entries.length) return;
if (entries.length === 1) {
if (entries[0].share) window.location.href = '/api/v1/shares/' + entries[0].share.id + '?type=download&path=' + encodeURIComponent(entries[0].filePath);
else window.location.href = '/api/v1/files?type=download&path=' + encodeURIComponent(entries[0].filePath);
return;
}
const params = new URLSearchParams();
// be a bit smart about the archive name and folder tree
const folderPath = entries[0].filePath.slice(0, -entries[0].fileName.length);
const archiveName = name || folderPath.slice(folderPath.slice(0, -1).lastIndexOf('/')+1).slice(0, -1);
params.append('name', archiveName);
params.append('skipPath', folderPath);
params.append('entries', JSON.stringify(entries.map(function (entry) {
return {
filePath: entry.filePath,
shareId: entry.share ? entry.share.id : undefined
};
})));
window.location.href = '/api/v1/download?' + params.toString();
}
function getFileTypeGroup(entry) {
return entry.mimeType.split('/')[0];
}
// simple extension detection, does not work with double extension like .tar.gz
function getExtension(entry) {
if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
return '';
}
function copyToClipboard(value) {
var elem = document.createElement('input');
elem.value = value;
document.body.append(elem);
elem.select();
document.execCommand('copy');
elem.remove();
}
function clearSelection() {
if(document.selection && document.selection.empty) {
document.selection.empty();
} else if(window.getSelection) {
var sel = window.getSelection();
sel.removeAllRanges();
}
}
function urlSearchQuery() {
return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
}
// those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
function parseResourcePath(resourcePath) {
var result = {
type: '',
path: '',
shareId: '',
apiPath: '',
resourcePath: ''
};
if (resourcePath.indexOf('files/') === 0) {
result.type = 'files';
result.path = resourcePath.slice('files'.length) || '/';
result.apiPath = '/api/v1/files';
result.resourcePath = result.type + result.path;
} else if (resourcePath.indexOf('shares/') === 0) {
result.type = 'shares';
result.shareId = resourcePath.split('/')[1];
result.path = resourcePath.slice((result.type + '/' + result.shareId).length) || '/';
result.apiPath = '/api/v1/shares/' + result.shareId;
// without shareId we show the root (share listing)
result.resourcePath = result.type + '/' + (result.shareId ? (result.shareId + result.path) : '');
} else {
console.error('Unknown resource path', resourcePath);
}
return result;
}
function getEntryIdentifier(entry) {
return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
}
function entryListSort(list, prop, desc) {
var tmp = list.sort(function (a, b) {
var av = a[prop];
var bv = b[prop];
if (typeof av === 'string') return (av.toUpperCase() < bv.toUpperCase()) ? -1 : 1;
else return (av < bv) ? -1 : 1;
});
if (desc) return tmp;
return tmp.reverse();
}
export {
getDirectLink,
getPreviewUrl,
getShareLink,
getFileTypeGroup,
prettyDate,
prettyLongDate,
prettyFileSize,
sanitize,
encode,
decode,
download,
getExtension,
copyToClipboard,
clearSelection,
urlSearchQuery,
parseResourcePath,
getEntryIdentifier,
entryListSort
};
-470
View File
@@ -1,470 +0,0 @@
<template>
<MainLayout>
<template #dialogs>
<!-- have to use v-model instead of : bind - https://github.com/primefaces/primevue/issues/815 -->
<Dialog v-model:visible="newFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFileDialogNameInput')">
<template #header>
<label class="dialog-header" for="newFileDialogNameInput">New file name</label>
</template>
<template #default>
<form @submit="onNewFileDialogSubmit" @submit.prevent>
<InputText class="dialog-single-input" id="newFileDialogNameInput" v-model="newFileDialog.name" :disabled="newFileDialog.busy" required/>
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
</form>
</template>
</Dialog>
<Dialog v-model:visible="newFolderDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFolderDialogNameInput')">
<template #header>
<label class="dialog-header" for="newFolderDialogNameInput">New folder name</label>
</template>
<template #default>
<form @submit="onNewFolderDialogSubmit" @submit.prevent>
<InputText class="dialog-single-input" id="newFolderDialogNameInput" v-model="newFolderDialog.name" :disabled="newFolderDialog.busy" required/>
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
</form>
</template>
</Dialog>
</template>
<template #header>
<TopBar class="navbar">
<template #left>
<Button icon="pi pi-chevron-left" @click="onGoUp()" text :disabled="cwd === '/'"/>
<span style="margin-left: 20px;">{{ cwd }}</span>
</template>
<template #right>
<Button type="button" label="New" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 10px" />
<Menu ref="createMenu" id="create_menu" :model="createMenuModel" :popup="true" />
<Button type="button" label="Upload" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 10px" />
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
<Dropdown v-model="activeResource" filter :options="resourcesDropdownModel" optionLabel="label" optionGroupLabel="label" optionGroupChildren="items" dataKey="id" @change="onAppChange" placeholder="Select an App or Volume" style="margin-right: 10px" />
</template>
</TopBar>
</template>
<template #body>
<div class="main-view">
<div class="main-view-col">
<DirectoryView
:show-owner="true"
:show-size="true"
:show-modified="true"
@selection-changed="onSelectionChanged"
@item-activated="onItemActivated"
:delete-handler="deleteHandler"
:rename-handler="renameHandler"
:change-owner-handler="changeOwnerHandler"
:copy-handler="copyHandler"
:cut-handler="cutHandler"
:paste-handler="pasteHandler"
:new-file-handler="onNewFile"
:new-folder-handler="onNewFolder"
:upload-file-handler="onUploadFile"
:upload-folder-handler="onUploadFolder"
:drop-handler="onDrop"
:items="items"
:clipboard="clipboard"
:owners-model="ownersModel"
/>
</div>
<div class="main-view-col" style="max-width: 300px;">
<PreviewPanel :item="activeItem || activeDirectoryItem"/>
</div>
</div>
</template>
<template #footer>
<FileUploader
ref="fileUploader"
:upload-handler="uploadHandler"
@finished="onUploadFinished"
/>
<BottomBar />
</template>
</MainLayout>
</template>
<script>
import superagent from 'superagent';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import Menu from 'primevue/menu';
import { useConfirm } from 'primevue/useconfirm';
import { DirectoryView, TopBar, BottomBar, MainLayout, FileUploader } from 'pankow';
import { sanitize, buildFilePath } from 'pankow/utils';
import PreviewPanel from '../components/PreviewPanel.vue';
import { createDirectoryModel } from '../models/DirectoryModel.js';
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
const BASE_URL = import.meta.env.BASE_URL || '/';
export default {
name: 'Home',
components: {
BottomBar,
Button,
Dialog,
DirectoryView,
Dropdown,
FileUploader,
InputText,
MainLayout,
Menu,
PreviewPanel,
TopBar
},
data() {
return {
cwd: '/',
activeItem: null,
activeDirectoryItem: {},
items: [],
selectedItems: [],
clipboard: {
action: '', // copy or cut
files: []
},
accessToken: localStorage.token,
apiOrigin: API_ORIGIN || '',
apps: [],
volumes: [],
resources: [],
resourcesDropdownModel: [],
selectedAppId: '',
activeResource: null,
visible: true,
newFileDialog: {
visible: false,
busy: false,
name: ''
},
newFolderDialog: {
visible: false,
busy: false,
name: ''
},
ownersModel: [{
uid: 0,
label: 'root'
}, {
uid: 33,
label: 'www-data'
}, {
uid: 1000,
label: 'cloudron'
}, {
uid: 1001,
label: 'git'
}],
// contextMenuModel will have activeItem attached if any command() is called
createMenuModel: [{
label: 'File',
icon: 'pi pi-file',
command: this.onNewFile
}, {
label: 'Folder',
icon: 'pi pi-folder',
command: this.onNewFolder
}],
uploadMenuModel: [{
label: 'File',
icon: 'pi pi-file',
command: this.onUploadFile
}, {
label: 'Folder',
icon: 'pi pi-folder',
command: this.onUploadFolder
}]
};
},
watch: {
cwd(newCwd, oldCwd) {
if (this.activeResource) this.$router.push(`/home/${this.activeResource.type}/${this.activeResource.id}${this.cwd}`);
this.loadCwd();
}
},
methods: {
onCreateMenu(event) {
this.$refs.createMenu.toggle(event);
},
onUploadMenu(event) {
this.$refs.uploadMenu.toggle(event);
},
// generic dialog focus handler
onDialogShow(focusElementId) {
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
},
onNewFile() {
this.newFileDialog.busy = false;
this.newFileDialog.name = '';
this.newFileDialog.visible = true;
},
async onNewFileDialogSubmit() {
this.newFileDialog.busy = true;
await this.directoryModel.newFile(buildFilePath(this.cwd, this.newFileDialog.name), this.newFileDialog.name);
await this.loadCwd();
this.newFileDialog.visible = false;
},
onNewFolder() {
this.newFolderDialog.busy = false;
this.newFolderDialog.name = '';
this.newFolderDialog.visible = true;
},
async onNewFolderDialogSubmit() {
this.newFolderDialog.busy = true;
await this.directoryModel.newFolder(buildFilePath(this.cwd, this.newFolderDialog.name));
await this.loadCwd();
this.newFolderDialog.visible = false;
},
onUploadFile() {
this.$refs.fileUploader.onUploadFile(this.cwd);
},
onUploadFolder() {
this.$refs.fileUploader.onUploadFolder(this.cwd);
},
onUploadFinished() {
this.loadCwd();
},
onAppChange(event) {
this.$router.push(`/home/${event.value.type}/${event.value.id}`);
this.cwd = '/';
this.loadResource(event.value);
},
onSelectionChanged(items) {
this.activeItem = items[0] || null;
this.selectedItems = items;
},
onGoUp() {
this.cwd = sanitize(this.cwd.split('/').slice(0, -1).join('/'));
},
async onDrop(targetFolder, dataTransfer) {
const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
let folderItem;
try {
folderItem = dataTransfer.items[0].webkitGetAsEntry();
if (folderItem.isFile) return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
} catch (e) {
return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
}
// if we got here we have a folder drop and a modern browser
// now traverse the folder tree and create a file list
var that = this;
function traverseFileTree(item, path) {
if (item.isFile) {
item.file(function (file) {
that.$refs.fileUploader.addFiles([file], sanitize(`${that.cwd}/${targetFolder}`), false);
});
} else if (item.isDirectory) {
// Get folder contents
var dirReader = item.createReader();
dirReader.readEntries(function (entries) {
for (let i in entries) {
traverseFileTree(entries[i], item.name);
}
});
}
}
traverseFileTree(folderItem, '');
},
onItemActivated(item) {
if (!item) return;
if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
else this.$router.push(`/viewer/${this.activeResource.type}/${this.activeResource.id}${sanitize(this.cwd + '/' + item.name)}`);
},
async deleteHandler(files) {
if (!files) return;
for (let i in files) {
await this.directoryModel.remove(buildFilePath(this.cwd, files[i].name));
}
await this.loadCwd();
},
async renameHandler(file, newName) {
await this.directoryModel.rename(buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
await this.loadCwd();
},
async changeOwnerHandler(files, newOwnerUid) {
if (!files) return;
for (let i in files) {
await this.directoryModel.chown(buildFilePath(this.cwd, files[i].name), newOwnerUid);
}
await this.loadCwd();
},
async copyHandler(files) {
if (!files) return;
this.clipboard = {
action: 'copy',
files
};
},
async cutHandler(files) {
if (!files) return;
this.clipboard = {
action: 'cut',
files
};
},
async pasteHandler(target) {
if (!this.clipboard.files || !this.clipboard.files.length) return;
const targetPath = target ? sanitize(this.cwd + '/' + target.fileName) : this.cwd;
await this.directoryModel.paste(targetPath, this.clipboard.action, this.clipboard.files);
this.clipboard = {};
await this.loadCwd();
},
async uploadHandler(targetDir, file, progressHandler) {
await this.directoryModel.upload(targetDir, file, progressHandler);
await this.loadCwd();
},
async loadCwd() {
this.items = await this.directoryModel.listFiles(this.cwd);
const tmp = this.cwd.split('/').slice(1);
let name = this.activeResource.fqdn;
if (tmp.length > 1) name = tmp[tmp.length-2];
this.activeDirectoryItem = {
id: name,
name: name,
type: 'directory',
mimeType: 'inode/directory',
icon: `${BASE_URL}mime-types/inode-directory.svg`
};
},
async loadResource(resource) {
this.activeResource = resource;
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, resource.type === 'volume' ? `volumes/${resource.id}` : `apps/${resource.id}`);
this.loadCwd();
}
},
async mounted() {
useConfirm();
// load all apps
let error, result;
try {
result = await superagent.get(`${this.apiOrigin}/api/v1/apps`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (error || result.statusCode !== 200) {
console.error('Failed to list apps', error || result.statusCode);
this.apps = [];
} else {
this.apps = result.body ? result.body.apps.filter(a => !!a.manifest.addons.localstorage) : [];
}
this.apps.forEach(function (a) { a.type = 'app'; a.label = a.fqdn; });
// load all volumes
try {
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (error || result.statusCode !== 200) {
console.error('Failed to list volumes', error || result.statusCode);
this.volumes = [];
} else {
this.volumes = result.body ? result.body.volumes : [];
}
this.volumes.forEach(function (a) { a.type = 'volume'; a.label = a.name; });
this.resources = this.apps.concat(this.volumes);
this.resourcesDropdownModel = [{
label: 'Apps',
items: this.apps
}, {
label: 'Volumes',
items: this.volumes
}];
const type = this.$route.params.type || 'app';
const resourceId = this.$route.params.resourceId;
if (type === 'volume') {
this.activeResource = this.volumes.find(a => a.id === resourceId);
if (!this.activeResource) this.activeResource = this.volumes[0];
if (!this.activeResource) return console.error('Unable to find volumes', resourceId);
} else if (type === 'app') {
this.activeResource = this.apps.find(a => a.id === resourceId);
if (!this.activeResource) this.activeResource = this.apps[0];
if (!this.activeResource) return console.error('Unable to find app', resourceId);
} else {
this.activeResource = this.apps[0];
}
if (!this.activeResource) {
console.error('Not able to load apps or volumes. Cannot continue');
return;
}
this.cwd = sanitize('/' + (this.$route.params.cwd ? this.$route.params.cwd.join('/') : '/'));
this.loadResource(this.activeResource);
this.$watch(() => this.$route.params, (toParams, previousParams) => {
if (toParams.type === 'volume') {
this.activeResource = this.volumes.find(a => a.id === toParams.resourceId);
} else if (toParams.type === 'app') {
this.activeResource = this.apps.find(a => a.id === toParams.resourceId);
} else {
console.error(`Unknown type ${toParams.type}`);
}
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
});
}
};
</script>
<style scoped>
.main-view {
flex-grow: 1;
overflow: hidden;
height: 100%;
display: flex;
padding: 0 10px
}
.main-view-col {
overflow: auto;
flex-grow: 1;
}
.dialog-header {
font-weight: 600;
font-size: 1.25rem;
}
.dialog-single-input {
display: block;
width: 100%;
margin-top: 5px;
margin-bottom: 1.5rem;
}
.dialog-single-input-submit {
margin-top: 5px;
}
</style>
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
fs: {
// Allow serving files from one level up to the project root for monaco editor assets
allow: ['..']
},
},
});
+15
View File
@@ -0,0 +1,15 @@
# Dashboard Filemanager
Local development via:
```
VITE_API_ORIGIN=my.example.com npm run dev
```
It requires an access token in `localStorage.token`.
The default local urls look like:
FileManager: `http://localhost:5173/filemanager.html#/home/app/<appId>`
Terminal: `http://localhost:5173/terminal.html?id=<appId>`
LogViewer: `http://localhost:5173/logs.html?appId=<appId>`
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
./node_modules/.bin/vite build --base=/frontend/
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/api/v1/cloudron/avatar" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Manager</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/filemanager.js"></script>
</body>
</html>
@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/logo.png" />
<link rel="icon" href="/api/v1/cloudron/avatar" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FileManager</title>
<title>Logs</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/logs.js"></script>
</body>
</html>
+381 -228
View File
@@ -8,24 +8,32 @@
"name": "my-vue-app",
"version": "0.0.0",
"dependencies": {
"@fontsource/noto-sans": "^5.0.9",
"anser": "^2.1.1",
"combokeys": "^3.0.1",
"filesize": "^10.0.7",
"pankow": "^0.1.2",
"filesize": "^10.0.12",
"marked": "^7.0.4",
"moment": "^2.29.4",
"pankow": "^0.6.1",
"primeicons": "^6.0.1",
"primevue": "^3.27.0",
"superagent": "^8.0.9",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"primevue": "^3.32.1",
"superagent": "^8.1.2",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"xterm": "^5.2.1",
"xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.1",
"vite": "^4.3.3"
"@vitejs/plugin-vue": "^4.3.2",
"vite": "^4.4.9"
}
},
"node_modules/@babel/parser": {
"version": "7.20.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz",
"integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==",
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
"integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -34,9 +42,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.14.tgz",
"integrity": "sha512-0CnlwnjDU8cks0yJLXfkaU/uoLyRf9VZJs4p1PskBr2AlAHeEsFEwJEo0of/Z3g+ilw5mpyDwThlxzNEIxOE4g==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.11.tgz",
"integrity": "sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==",
"cpu": [
"arm"
],
@@ -50,9 +58,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.14.tgz",
"integrity": "sha512-eLOpPO1RvtsP71afiFTvS7tVFShJBCT0txiv/xjFBo5a7R7Gjw7X0IgIaFoLKhqXYAXhahoXm7qAmRXhY4guJg==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz",
"integrity": "sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==",
"cpu": [
"arm64"
],
@@ -66,9 +74,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.14.tgz",
"integrity": "sha512-nrfQYWBfLGfSGLvRVlt6xi63B5IbfHm3tZCdu/82zuFPQ7zez4XjmRtF/wIRYbJQ/DsZrxJdEvYFE67avYXyng==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.11.tgz",
"integrity": "sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==",
"cpu": [
"x64"
],
@@ -82,9 +90,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.14.tgz",
"integrity": "sha512-eoSjEuDsU1ROwgBH/c+fZzuSyJUVXQTOIN9xuLs9dE/9HbV/A5IqdXHU1p2OfIMwBwOYJ9SFVGGldxeRCUJFyw==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz",
"integrity": "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==",
"cpu": [
"arm64"
],
@@ -98,9 +106,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.14.tgz",
"integrity": "sha512-zN0U8RWfrDttdFNkHqFYZtOH8hdi22z0pFm0aIJPsNC4QQZv7je8DWCX5iA4Zx6tRhS0CCc0XC2m7wKsbWEo5g==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz",
"integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==",
"cpu": [
"x64"
],
@@ -114,9 +122,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.14.tgz",
"integrity": "sha512-z0VcD4ibeZWVQCW1O7szaLxGsx54gcCnajEJMdYoYjLiq4g1jrP2lMq6pk71dbS5+7op/L2Aod+erw+EUr28/A==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz",
"integrity": "sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==",
"cpu": [
"arm64"
],
@@ -130,9 +138,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.14.tgz",
"integrity": "sha512-hd9mPcxfTgJlolrPlcXkQk9BMwNBvNBsVaUe5eNUqXut6weDQH8whcNaKNF2RO8NbpT6GY8rHOK2A9y++s+ehw==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz",
"integrity": "sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==",
"cpu": [
"x64"
],
@@ -146,9 +154,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.14.tgz",
"integrity": "sha512-BNTl+wSJ1omsH8s3TkQmIIIQHwvwJrU9u1ggb9XU2KTVM4TmthRIVyxSp2qxROJHhZuW/r8fht46/QE8hU8Qvg==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz",
"integrity": "sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==",
"cpu": [
"arm"
],
@@ -162,9 +170,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.14.tgz",
"integrity": "sha512-FhAMNYOq3Iblcj9i+K0l1Fp/MHt+zBeRu/Qkf0LtrcFu3T45jcwB6A1iMsemQ42vR3GBhjNZJZTaCe3VFPbn9g==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz",
"integrity": "sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==",
"cpu": [
"arm64"
],
@@ -178,9 +186,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.14.tgz",
"integrity": "sha512-91OK/lQ5y2v7AsmnFT+0EyxdPTNhov3y2CWMdizyMfxSxRqHazXdzgBKtlmkU2KYIc+9ZK3Vwp2KyXogEATYxQ==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz",
"integrity": "sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==",
"cpu": [
"ia32"
],
@@ -194,9 +202,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.14.tgz",
"integrity": "sha512-vp15H+5NR6hubNgMluqqKza85HcGJgq7t6rMH7O3Y6ApiOWPkvW2AJfNojUQimfTp6OUrACUXfR4hmpcENXoMQ==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz",
"integrity": "sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==",
"cpu": [
"loong64"
],
@@ -210,9 +218,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.14.tgz",
"integrity": "sha512-90TOdFV7N+fgi6c2+GO9ochEkmm9kBAKnuD5e08GQMgMINOdOFHuYLPQ91RYVrnWwQ5683sJKuLi9l4SsbJ7Hg==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz",
"integrity": "sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==",
"cpu": [
"mips64el"
],
@@ -226,9 +234,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.14.tgz",
"integrity": "sha512-NnBGeoqKkTugpBOBZZoktQQ1Yqb7aHKmHxsw43NddPB2YWLAlpb7THZIzsRsTr0Xw3nqiPxbA1H31ZMOG+VVPQ==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz",
"integrity": "sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==",
"cpu": [
"ppc64"
],
@@ -242,9 +250,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.14.tgz",
"integrity": "sha512-0qdlKScLXA8MGVy21JUKvMzCYWovctuP8KKqhtE5A6IVPq4onxXhSuhwDd2g5sRCzNDlDjitc5sX31BzDoL5Fw==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz",
"integrity": "sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==",
"cpu": [
"riscv64"
],
@@ -258,9 +266,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.14.tgz",
"integrity": "sha512-Hdm2Jo1yaaOro4v3+6/zJk6ygCqIZuSDJHdHaf8nVH/tfOuoEX5Riv03Ka15LmQBYJObUTNS1UdyoMk0WUn9Ww==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz",
"integrity": "sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==",
"cpu": [
"s390x"
],
@@ -274,9 +282,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.14.tgz",
"integrity": "sha512-8KHF17OstlK4DuzeF/KmSgzrTWQrkWj5boluiiq7kvJCiQVzUrmSkaBvcLB2UgHpKENO2i6BthPkmUhNDaJsVw==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz",
"integrity": "sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==",
"cpu": [
"x64"
],
@@ -290,9 +298,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.14.tgz",
"integrity": "sha512-nVwpqvb3yyXztxIT2+VsxJhB5GCgzPdk1n0HHSnchRAcxqKO6ghXwHhJnr0j/B+5FSyEqSxF4q03rbA2fKXtUQ==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz",
"integrity": "sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==",
"cpu": [
"x64"
],
@@ -306,9 +314,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.14.tgz",
"integrity": "sha512-1RZ7uQQ9zcy/GSAJL1xPdN7NDdOOtNEGiJalg/MOzeakZeTrgH/DoCkbq7TaPDiPhWqnDF+4bnydxRqQD7il6g==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz",
"integrity": "sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==",
"cpu": [
"x64"
],
@@ -322,9 +330,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.14.tgz",
"integrity": "sha512-nqMjDsFwv7vp7msrwWRysnM38Sd44PKmW8EzV01YzDBTcTWUpczQg6mGao9VLicXSgW/iookNK6AxeogNVNDZA==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz",
"integrity": "sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==",
"cpu": [
"x64"
],
@@ -338,9 +346,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.14.tgz",
"integrity": "sha512-xrD0mccTKRBBIotrITV7WVQAwNJ5+1va6L0H9zN92v2yEdjfAN7864cUaZwJS7JPEs53bDTzKFbfqVlG2HhyKQ==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz",
"integrity": "sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==",
"cpu": [
"arm64"
],
@@ -354,9 +362,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.14.tgz",
"integrity": "sha512-nXpkz9bbJrLLyUTYtRotSS3t5b+FOuljg8LgLdINWFs3FfqZMtbnBCZFUmBzQPyxqU87F8Av+3Nco/M3hEcu1w==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz",
"integrity": "sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==",
"cpu": [
"ia32"
],
@@ -370,9 +378,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.14.tgz",
"integrity": "sha512-gPQmsi2DKTaEgG14hc3CHXHp62k8g6qr0Pas+I4lUxRMugGSATh/Bi8Dgusoz9IQ0IfdrvLpco6kujEIBoaogA==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz",
"integrity": "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==",
"cpu": [
"x64"
],
@@ -385,6 +393,73 @@
"node": ">=12"
}
},
"node_modules/@fontsource/noto-sans": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/@fontsource/noto-sans/-/noto-sans-5.0.9.tgz",
"integrity": "sha512-SvZDr+KJJ+J5Kx05l2qtss72yEJd0YdgvNxwAwO6gDKQqTbWJ5kUi0S9TBmVoQGw0gvQtbtlG9wBz8NQygjCyg=="
},
"node_modules/@intlify/core-base": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.2.2.tgz",
"integrity": "sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==",
"dependencies": {
"@intlify/devtools-if": "9.2.2",
"@intlify/message-compiler": "9.2.2",
"@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.2.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/devtools-if": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.2.2.tgz",
"integrity": "sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==",
"dependencies": {
"@intlify/shared": "9.2.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.2.2.tgz",
"integrity": "sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==",
"dependencies": {
"@intlify/shared": "9.2.2",
"source-map": "0.6.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/shared": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.2.2.tgz",
"integrity": "sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==",
"engines": {
"node": ">= 14"
}
},
"node_modules/@intlify/vue-devtools": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz",
"integrity": "sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==",
"dependencies": {
"@intlify/core-base": "9.2.2",
"@intlify/shared": "9.2.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@types/node": {
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
@@ -394,9 +469,9 @@
"peer": true
},
"node_modules/@vitejs/plugin-vue": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.1.tgz",
"integrity": "sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.3.2.tgz",
"integrity": "sha512-iDDhGruwhKkwNwT5qgtGaeTxF4ULs52xpQbsC27F01kf3aQBHtrDP738pmHw4oclVAUA3m+Vk8gxhDV5KbfM+A==",
"dev": true,
"engines": {
"node": "^14.18.0 || >=16.0.0"
@@ -407,49 +482,49 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz",
"integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/shared": "3.2.47",
"@babel/parser": "^7.21.3",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"source-map": "^0.6.1"
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz",
"integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
"dependencies": {
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47"
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz",
"integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.47",
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-ssr": "3.2.47",
"@vue/reactivity-transform": "3.2.47",
"@vue/shared": "3.2.47",
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-ssr": "3.3.4",
"@vue/reactivity-transform": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7",
"magic-string": "^0.30.0",
"postcss": "^8.1.10",
"source-map": "^0.6.1"
"source-map-js": "^1.0.2"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz",
"integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
"dependencies": {
"@vue/compiler-dom": "3.2.47",
"@vue/shared": "3.2.47"
"@vue/compiler-dom": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/devtools-api": {
@@ -458,60 +533,65 @@
"integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
},
"node_modules/@vue/reactivity": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz",
"integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
"dependencies": {
"@vue/shared": "3.2.47"
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/reactivity-transform": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz",
"integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
"dependencies": {
"@babel/parser": "^7.16.4",
"@vue/compiler-core": "3.2.47",
"@vue/shared": "3.2.47",
"@babel/parser": "^7.20.15",
"@vue/compiler-core": "3.3.4",
"@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
"magic-string": "^0.25.7"
"magic-string": "^0.30.0"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz",
"integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
"dependencies": {
"@vue/reactivity": "3.2.47",
"@vue/shared": "3.2.47"
"@vue/reactivity": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz",
"integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
"dependencies": {
"@vue/runtime-core": "3.2.47",
"@vue/shared": "3.2.47",
"csstype": "^2.6.8"
"@vue/runtime-core": "3.3.4",
"@vue/shared": "3.3.4",
"csstype": "^3.1.1"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz",
"integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
"dependencies": {
"@vue/compiler-ssr": "3.2.47",
"@vue/shared": "3.2.47"
"@vue/compiler-ssr": "3.3.4",
"@vue/shared": "3.3.4"
},
"peerDependencies": {
"vue": "3.2.47"
"vue": "3.3.4"
}
},
"node_modules/@vue/shared": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz",
"integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ=="
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
},
"node_modules/anser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/anser/-/anser-2.1.1.tgz",
"integrity": "sha512-nqLm4HxOTpeLOxcmB3QWmV5TcDFhW9y/fyQ+hivtDFcK4OQ+pQ5fzPnXHM1Mfcm0VkLtvVi1TCPr++Qy0Q/3EQ=="
},
"node_modules/asap": {
"version": "2.0.6",
@@ -562,9 +642,9 @@
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/csstype": {
"version": "2.6.21",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/debug": {
"version": "4.3.4",
@@ -600,9 +680,9 @@
}
},
"node_modules/esbuild": {
"version": "0.17.14",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.14.tgz",
"integrity": "sha512-vOO5XhmVj/1XQR9NQ1UPq6qvMYL7QFJU57J5fKBKBKxp17uDt5PgxFDb4A2nEiXhr1qQs4x0F5+66hVVw4ruNw==",
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.11.tgz",
"integrity": "sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==",
"dev": true,
"hasInstallScript": true,
"bin": {
@@ -612,28 +692,28 @@
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.17.14",
"@esbuild/android-arm64": "0.17.14",
"@esbuild/android-x64": "0.17.14",
"@esbuild/darwin-arm64": "0.17.14",
"@esbuild/darwin-x64": "0.17.14",
"@esbuild/freebsd-arm64": "0.17.14",
"@esbuild/freebsd-x64": "0.17.14",
"@esbuild/linux-arm": "0.17.14",
"@esbuild/linux-arm64": "0.17.14",
"@esbuild/linux-ia32": "0.17.14",
"@esbuild/linux-loong64": "0.17.14",
"@esbuild/linux-mips64el": "0.17.14",
"@esbuild/linux-ppc64": "0.17.14",
"@esbuild/linux-riscv64": "0.17.14",
"@esbuild/linux-s390x": "0.17.14",
"@esbuild/linux-x64": "0.17.14",
"@esbuild/netbsd-x64": "0.17.14",
"@esbuild/openbsd-x64": "0.17.14",
"@esbuild/sunos-x64": "0.17.14",
"@esbuild/win32-arm64": "0.17.14",
"@esbuild/win32-ia32": "0.17.14",
"@esbuild/win32-x64": "0.17.14"
"@esbuild/android-arm": "0.18.11",
"@esbuild/android-arm64": "0.18.11",
"@esbuild/android-x64": "0.18.11",
"@esbuild/darwin-arm64": "0.18.11",
"@esbuild/darwin-x64": "0.18.11",
"@esbuild/freebsd-arm64": "0.18.11",
"@esbuild/freebsd-x64": "0.18.11",
"@esbuild/linux-arm": "0.18.11",
"@esbuild/linux-arm64": "0.18.11",
"@esbuild/linux-ia32": "0.18.11",
"@esbuild/linux-loong64": "0.18.11",
"@esbuild/linux-mips64el": "0.18.11",
"@esbuild/linux-ppc64": "0.18.11",
"@esbuild/linux-riscv64": "0.18.11",
"@esbuild/linux-s390x": "0.18.11",
"@esbuild/linux-x64": "0.18.11",
"@esbuild/netbsd-x64": "0.18.11",
"@esbuild/openbsd-x64": "0.18.11",
"@esbuild/sunos-x64": "0.18.11",
"@esbuild/win32-arm64": "0.18.11",
"@esbuild/win32-ia32": "0.18.11",
"@esbuild/win32-x64": "0.18.11"
}
},
"node_modules/estree-walker": {
@@ -647,9 +727,9 @@
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"node_modules/filesize": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.7.tgz",
"integrity": "sha512-iMRG7Qo9nayLoU3PNCiLizYtsy4W1ClrapeCwEgtiQelOAOuRJiw4QaLI+sSr8xr901dgHv+EYP2bCusGZgoiA==",
"version": "10.0.12",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-10.0.12.tgz",
"integrity": "sha512-6RS9gDchbn+qWmtV2uSjo5vmKizgfCQeb5jKmqx8HyzA3MoLqqyQxN+QcjkGBJt7FjJ9qFce67Auyya5rRRbpw==",
"engines": {
"node": ">= 10.4.0"
}
@@ -701,12 +781,13 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-intrinsic": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3"
},
"funding": {
@@ -724,6 +805,17 @@
"node": ">= 0.4.0"
}
},
"node_modules/has-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
@@ -755,11 +847,25 @@
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
"integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
"dependencies": {
"sourcemap-codec": "^1.4.8"
"@jridgewell/sourcemap-codec": "^1.4.13"
},
"engines": {
"node": ">=12"
}
},
"node_modules/marked": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz",
"integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/methods": {
@@ -800,10 +906,18 @@
"node": ">= 0.6"
}
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": {
"node": "*"
}
},
"node_modules/monaco-editor": {
"version": "0.37.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.37.1.tgz",
"integrity": "sha512-jLXEEYSbqMkT/FuJLBZAVWGuhIb4JNwHE9kPTorAVmsdZ4UzHAfgWxLsVtD7pLRFaOwYPhNG9nUCpmFL1t/dIg=="
"version": "0.41.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.41.0.tgz",
"integrity": "sha512-1o4olnZJsiLmv5pwLEAmzHTE/5geLKQ07BrGxlF4Ri/AXAc2yyDGZwHjiTqD8D/ROKUZmwMA28A+yEowLNOEcA=="
},
"node_modules/ms": {
"version": "2.1.2",
@@ -844,14 +958,14 @@
}
},
"node_modules/pankow": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/pankow/-/pankow-0.1.2.tgz",
"integrity": "sha512-JrVaqnIKzH762AAjxAyRMW4T/Fm0DhN90aT57Geukb2g8WE7qhBlSOgcFCFu+4U9SGUSy3mIRJaq1K1jdjFXiA==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/pankow/-/pankow-0.6.1.tgz",
"integrity": "sha512-gLNoSbnyVf3OCO7HjPzW2PcKawuAQsMUJnwbnH2k/DAarHtBXsuE+a6bs6rbHM66/ezz7ogBM2fYOsDOI01LMQ==",
"dependencies": {
"filesize": "^10.0.7",
"monaco-editor": "^0.37.1",
"primevue": "^3.27.0",
"superagent": "^8.0.9"
"filesize": "^10.0.12",
"monaco-editor": "^0.41.0",
"primevue": "^3.32.1",
"superagent": "^8.1.2"
}
},
"node_modules/picocolors": {
@@ -860,9 +974,9 @@
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/postcss": {
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"version": "8.4.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
"funding": [
{
"type": "opencollective",
@@ -892,17 +1006,17 @@
"integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA=="
},
"node_modules/primevue": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-3.27.0.tgz",
"integrity": "sha512-oVJl8vLGNb6t5nXN41mnjR5V9Cc/eHVvmtRWiNgIC1db6OW3Qo7y2LaDEmXps/wdxX/FuJ7nuPHAZI4y8tvGyQ==",
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-3.32.1.tgz",
"integrity": "sha512-SXeakYM2ZrRm7JHJ3pCqI0Nw2lHwZGXelt8oEsaG00uzHP+E2bDc2dLL2DrcHG5eLtp0E4ZSFmIYW4+q5joJFw==",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
"integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
"dependencies": {
"side-channel": "^1.0.4"
},
@@ -914,9 +1028,9 @@
}
},
"node_modules/rollup": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.0.tgz",
"integrity": "sha512-ANPhVcyeHvYdQMUyCbczy33nbLzI7RzrBje4uvNiTDJGIMtlKoOStmympwr9OtS1LZxiDmE2wvxHyVhoLtf1KQ==",
"version": "3.28.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.0.tgz",
"integrity": "sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@@ -930,9 +1044,9 @@
}
},
"node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -972,16 +1086,10 @@
"node": ">=0.10.0"
}
},
"node_modules/sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"deprecated": "Please use @jridgewell/sourcemap-codec instead"
},
"node_modules/superagent": {
"version": "8.0.9",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz",
"integrity": "sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA==",
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
"integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
"dependencies": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.4",
@@ -999,14 +1107,14 @@
}
},
"node_modules/vite": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.3.tgz",
"integrity": "sha512-MwFlLBO4udZXd+VBcezo3u8mC77YQk+ik+fbc0GZWGgzfbPP+8Kf0fldhARqvSYmtIWoAJ5BXPClUbMTlqFxrA==",
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
"integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
"dev": true,
"dependencies": {
"esbuild": "^0.17.5",
"postcss": "^8.4.23",
"rollup": "^3.21.0"
"esbuild": "^0.18.10",
"postcss": "^8.4.27",
"rollup": "^3.27.1"
},
"bin": {
"vite": "bin/vite.js"
@@ -1014,12 +1122,16 @@
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
},
"peerDependencies": {
"@types/node": ">= 14",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"stylus": "*",
"sugarss": "*",
@@ -1032,6 +1144,9 @@
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
@@ -1047,23 +1162,40 @@
}
},
"node_modules/vue": {
"version": "3.2.47",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz",
"integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==",
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
"dependencies": {
"@vue/compiler-dom": "3.2.47",
"@vue/compiler-sfc": "3.2.47",
"@vue/runtime-dom": "3.2.47",
"@vue/server-renderer": "3.2.47",
"@vue/shared": "3.2.47"
"@vue/compiler-dom": "3.3.4",
"@vue/compiler-sfc": "3.3.4",
"@vue/runtime-dom": "3.3.4",
"@vue/server-renderer": "3.3.4",
"@vue/shared": "3.3.4"
}
},
"node_modules/vue-i18n": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.2.2.tgz",
"integrity": "sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==",
"dependencies": {
"@intlify/core-base": "9.2.2",
"@intlify/shared": "9.2.2",
"@intlify/vue-devtools": "9.2.2",
"@vue/devtools-api": "^6.2.1"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.1.6.tgz",
"integrity": "sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ==",
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz",
"integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==",
"dependencies": {
"@vue/devtools-api": "^6.4.5"
"@vue/devtools-api": "^6.5.0"
},
"funding": {
"url": "https://github.com/sponsors/posva"
@@ -1077,6 +1209,27 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xterm": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.2.1.tgz",
"integrity": "sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA=="
},
"node_modules/xterm-addon-attach": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.8.0.tgz",
"integrity": "sha512-k8N5boSYn6rMJTTNCgFpiSTZ26qnYJf3v/nJJYexNO2sdAHDN3m1ivVQWVZ8CHJKKnZQw1rc44YP2NtgalWHfQ==",
"peerDependencies": {
"xterm": "^5.0.0"
}
},
"node_modules/xterm-addon-fit": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.7.0.tgz",
"integrity": "sha512-tQgHGoHqRTgeROPnvmtEJywLKoC/V9eNs4bLLz7iyJr1aW/QFzRwfd3MGiJ6odJd9xEfxcW36/xRU47JkD5NKQ==",
"peerDependencies": {
"xterm": "^5.0.0"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+33
View File
@@ -0,0 +1,33 @@
{
"name": "my-vue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fontsource/noto-sans": "^5.0.9",
"anser": "^2.1.1",
"combokeys": "^3.0.1",
"filesize": "^10.0.12",
"marked": "^7.0.4",
"moment": "^2.29.4",
"pankow": "^0.6.1",
"primeicons": "^6.0.1",
"primevue": "^3.32.1",
"superagent": "^8.1.2",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.4",
"xterm": "^5.2.1",
"xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.3.2",
"vite": "^4.4.9"
}
}

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

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